mirror of
https://github.com/Qortal/qortal.git
synced 2025-08-05 10:11:26 +00:00
Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0594bdf1c7 | ||
|
72c299a331 | ||
|
0b42a7ad63 | ||
|
51e59f6ab7 | ||
|
38394de661 | ||
|
22f9755f4f | ||
|
4cb2e113cb | ||
|
e0f024ef5c | ||
|
f95cb99cdc | ||
|
1f0170bb4b | ||
|
5eafdf3c80 | ||
|
d7c26c27e1 | ||
|
2d18dd62eb | ||
|
51fd177d79 | ||
|
c4643538f1 | ||
|
0edadaf901 | ||
|
c05533fb71 | ||
|
db270f559f | ||
|
79f7f68b0c | ||
|
d30d61edab | ||
|
f7e2ee383e | ||
|
544fdbfbe9 | ||
|
c3d1ecb7e1 | ||
|
873a9d0cee | ||
|
95cb5f607b | ||
|
54d0b721c4 | ||
|
4a4678b331 | ||
|
12f9ecaaca | ||
|
1d3ee77fb8 | ||
|
64055e280d | ||
|
90e0f9dddc | ||
|
b0b0e2ac18 | ||
|
9db606af5a | ||
|
5bfc17bd64 | ||
|
a3c44428d3 | ||
|
450ff7318f | ||
|
2dffd382ae | ||
|
e425fe5d5a | ||
|
a68caa2de1 | ||
|
3470b8bf57 | ||
|
99e11d1f52 | ||
|
743db9190e | ||
|
53b3d09288 | ||
|
4565d0ddcb | ||
|
de51de1819 |
BIN
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed-sources.jar
Normal file
BIN
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed-sources.jar
Normal file
Binary file not shown.
BIN
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed.jar
Normal file
BIN
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed.jar
Normal file
Binary file not shown.
9
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed.pom
Normal file
9
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed.pom
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>2.5.0-fixed</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
12
lib/org/hsqldb/hsqldb/maven-metadata-local.xml
Normal file
12
lib/org/hsqldb/hsqldb/maven-metadata-local.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<versioning>
|
||||
<release>2.5.0-fixed</release>
|
||||
<versions>
|
||||
<version>2.5.0-fixed</version>
|
||||
</versions>
|
||||
<lastUpdated>20200318133132</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
8
pom.xml
8
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>1.0</version>
|
||||
<version>1.0.5</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<bitcoin.version>0.15.4</bitcoin.version>
|
||||
@@ -13,7 +13,7 @@
|
||||
<commons-text.version>1.8</commons-text.version>
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
<guava.version>28.1-jre</guava.version>
|
||||
<hsqldb.version>2.5.0</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.22.v20191022</jetty.version>
|
||||
@@ -257,6 +257,8 @@
|
||||
<!-- Don't include original swagger-UI as we're including our own
|
||||
modified version -->
|
||||
<exclude>org.webjars:swagger-ui</exclude>
|
||||
<!-- Don't include JUnit as it's for testing only! -->
|
||||
<exclude>junit:junit</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
<filters>
|
||||
@@ -379,12 +381,14 @@
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<scope>provided</scope><!-- needed for build, not for runtime -->
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.github.bohnman/package-info-maven-plugin -->
|
||||
<dependency>
|
||||
<groupId>com.github.bohnman</groupId>
|
||||
<artifactId>package-info-maven-plugin</artifactId>
|
||||
<version>${package-info-maven-plugin.version}</version>
|
||||
<scope>provided</scope><!-- needed for build, not for runtime -->
|
||||
</dependency>
|
||||
<!-- HSQLDB for repository -->
|
||||
<dependency>
|
||||
|
@@ -204,11 +204,15 @@ public class Account {
|
||||
* @throws DataException
|
||||
*/
|
||||
public boolean canMint() throws DataException {
|
||||
Integer level = this.getLevel();
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
if (accountData == null)
|
||||
return false;
|
||||
|
||||
Integer level = accountData.getLevel();
|
||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
|
||||
return true;
|
||||
|
||||
if (this.isFounder())
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
@@ -226,11 +230,15 @@ public class Account {
|
||||
* @throws DataException
|
||||
*/
|
||||
public boolean canRewardShare() throws DataException {
|
||||
Integer level = this.getLevel();
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
if (accountData == null)
|
||||
return false;
|
||||
|
||||
Integer level = accountData.getLevel();
|
||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
|
||||
return true;
|
||||
|
||||
if (this.isFounder())
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
@@ -264,10 +272,14 @@ public class Account {
|
||||
* @throws DataException
|
||||
*/
|
||||
public int getEffectiveMintingLevel() throws DataException {
|
||||
if (this.isFounder())
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
if (accountData == null)
|
||||
return 0;
|
||||
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
|
||||
|
||||
Integer level = this.getLevel();
|
||||
Integer level = accountData.getLevel();
|
||||
if (level == null)
|
||||
return 0;
|
||||
|
||||
@@ -290,7 +302,7 @@ public class Account {
|
||||
if (rewardShareData == null)
|
||||
return 0;
|
||||
|
||||
PublicKeyAccount rewardShareMinter = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
|
||||
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
||||
return rewardShareMinter.getEffectiveMintingLevel();
|
||||
}
|
||||
|
||||
|
@@ -7,10 +7,10 @@ import java.util.Map;
|
||||
|
||||
public enum ApiError {
|
||||
// COMMON
|
||||
UNKNOWN(0, 500),
|
||||
// UNKNOWN(0, 500),
|
||||
JSON(1, 400),
|
||||
NO_BALANCE(2, 422),
|
||||
NOT_YET_RELEASED(3, 422),
|
||||
// NO_BALANCE(2, 422),
|
||||
// NOT_YET_RELEASED(3, 422),
|
||||
UNAUTHORIZED(4, 403),
|
||||
REPOSITORY_ISSUE(5, 500),
|
||||
NON_PRODUCTION(6, 403),
|
||||
@@ -19,28 +19,28 @@ public enum ApiError {
|
||||
// VALIDATION
|
||||
INVALID_SIGNATURE(101, 400),
|
||||
INVALID_ADDRESS(102, 400),
|
||||
INVALID_SEED(103, 400),
|
||||
INVALID_AMOUNT(104, 400),
|
||||
INVALID_FEE(105, 400),
|
||||
INVALID_SENDER(106, 400),
|
||||
INVALID_RECIPIENT(107, 400),
|
||||
INVALID_NAME_LENGTH(108, 400),
|
||||
INVALID_VALUE_LENGTH(109, 400),
|
||||
INVALID_NAME_OWNER(110, 400),
|
||||
INVALID_BUYER(111, 400),
|
||||
// INVALID_SEED(103, 400),
|
||||
// INVALID_AMOUNT(104, 400),
|
||||
// INVALID_FEE(105, 400),
|
||||
// INVALID_SENDER(106, 400),
|
||||
// INVALID_RECIPIENT(107, 400),
|
||||
// INVALID_NAME_LENGTH(108, 400),
|
||||
// INVALID_VALUE_LENGTH(109, 400),
|
||||
// INVALID_NAME_OWNER(110, 400),
|
||||
// INVALID_BUYER(111, 400),
|
||||
INVALID_PUBLIC_KEY(112, 400),
|
||||
INVALID_OPTIONS_LENGTH(113, 400),
|
||||
INVALID_OPTION_LENGTH(114, 400),
|
||||
// INVALID_OPTIONS_LENGTH(113, 400),
|
||||
// INVALID_OPTION_LENGTH(114, 400),
|
||||
INVALID_DATA(115, 400),
|
||||
INVALID_DATA_LENGTH(116, 400),
|
||||
INVALID_UPDATE_VALUE(117, 400),
|
||||
KEY_ALREADY_EXISTS(118, 422),
|
||||
KEY_NOT_EXISTS(119, 404),
|
||||
LAST_KEY_IS_DEFAULT_KEY_ERROR(120, 422),
|
||||
FEE_LESS_REQUIRED(121, 422),
|
||||
WALLET_NOT_IN_SYNC(122, 422),
|
||||
// INVALID_DATA_LENGTH(116, 400),
|
||||
// INVALID_UPDATE_VALUE(117, 400),
|
||||
// KEY_ALREADY_EXISTS(118, 422),
|
||||
// KEY_NOT_EXISTS(119, 404),
|
||||
// LAST_KEY_IS_DEFAULT_KEY_ERROR(120, 422),
|
||||
// FEE_LESS_REQUIRED(121, 422),
|
||||
// WALLET_NOT_IN_SYNC(122, 422),
|
||||
INVALID_NETWORK_ADDRESS(123, 404),
|
||||
ADDRESS_NO_EXISTS(124, 404),
|
||||
ADDRESS_UNKNOWN(124, 404),
|
||||
INVALID_CRITERIA(125, 400),
|
||||
INVALID_REFERENCE(126, 400),
|
||||
TRANSFORMATION_ERROR(127, 400),
|
||||
@@ -49,72 +49,72 @@ public enum ApiError {
|
||||
CANNOT_MINT(130, 400),
|
||||
|
||||
// WALLET
|
||||
WALLET_NO_EXISTS(201, 404),
|
||||
WALLET_ADDRESS_NO_EXISTS(202, 404),
|
||||
WALLET_LOCKED(203, 422),
|
||||
WALLET_ALREADY_EXISTS(204, 422),
|
||||
WALLET_API_CALL_FORBIDDEN_BY_USER(205, 403),
|
||||
// WALLET_NO_EXISTS(201, 404),
|
||||
// WALLET_ADDRESS_NO_EXISTS(202, 404),
|
||||
// WALLET_LOCKED(203, 422),
|
||||
// WALLET_ALREADY_EXISTS(204, 422),
|
||||
// WALLET_API_CALL_FORBIDDEN_BY_USER(205, 403),
|
||||
|
||||
// BLOCKS
|
||||
BLOCK_NO_EXISTS(301, 404),
|
||||
BLOCK_UNKNOWN(301, 404),
|
||||
|
||||
// TRANSACTIONS
|
||||
TRANSACTION_NO_EXISTS(311, 404),
|
||||
TRANSACTION_UNKNOWN(311, 404),
|
||||
PUBLIC_KEY_NOT_FOUND(304, 404),
|
||||
TRANSACTION_INVALID(312, 400),
|
||||
|
||||
// NAMING
|
||||
NAME_NO_EXISTS(401, 404),
|
||||
NAME_ALREADY_EXISTS(402, 422),
|
||||
NAME_ALREADY_FOR_SALE(403, 422),
|
||||
NAME_NOT_LOWER_CASE(404, 422),
|
||||
NAME_SALE_NO_EXISTS(410, 404),
|
||||
BUYER_ALREADY_OWNER(411, 422),
|
||||
NAME_UNKNOWN(401, 404),
|
||||
// NAME_ALREADY_EXISTS(402, 422),
|
||||
// NAME_ALREADY_FOR_SALE(403, 422),
|
||||
// NAME_NOT_LOWER_CASE(404, 422),
|
||||
// NAME_SALE_NO_EXISTS(410, 404),
|
||||
// BUYER_ALREADY_OWNER(411, 422),
|
||||
|
||||
// POLLS
|
||||
POLL_NO_EXISTS(501, 404),
|
||||
POLL_ALREADY_EXISTS(502, 422),
|
||||
DUPLICATE_OPTION(503, 422),
|
||||
POLL_OPTION_NO_EXISTS(504, 404),
|
||||
ALREADY_VOTED_FOR_THAT_OPTION(505, 422),
|
||||
// POLL_NO_EXISTS(501, 404),
|
||||
// POLL_ALREADY_EXISTS(502, 422),
|
||||
// DUPLICATE_OPTION(503, 422),
|
||||
// POLL_OPTION_NO_EXISTS(504, 404),
|
||||
// ALREADY_VOTED_FOR_THAT_OPTION(505, 422),
|
||||
|
||||
// ASSET
|
||||
INVALID_ASSET_ID(601, 400),
|
||||
INVALID_ORDER_ID(602, 400),
|
||||
ORDER_NO_EXISTS(603, 404),
|
||||
ORDER_UNKNOWN(603, 404),
|
||||
|
||||
// NAME PAYMENTS
|
||||
NAME_NOT_REGISTERED(701, 422),
|
||||
NAME_FOR_SALE(702, 422),
|
||||
NAME_WITH_SPACE(703, 422),
|
||||
// NAME_NOT_REGISTERED(701, 422),
|
||||
// NAME_FOR_SALE(702, 422),
|
||||
// NAME_WITH_SPACE(703, 422),
|
||||
|
||||
// ATs
|
||||
INVALID_DESC_LENGTH(801, 400),
|
||||
EMPTY_CODE(802, 400),
|
||||
DATA_SIZE(803, 400),
|
||||
NULL_PAGES(804, 400),
|
||||
INVALID_TYPE_LENGTH(805, 400),
|
||||
INVALID_TAGS_LENGTH(806, 400),
|
||||
INVALID_CREATION_BYTES(809, 400),
|
||||
// INVALID_DESC_LENGTH(801, 400),
|
||||
// EMPTY_CODE(802, 400),
|
||||
// DATA_SIZE(803, 400),
|
||||
// NULL_PAGES(804, 400),
|
||||
// INVALID_TYPE_LENGTH(805, 400),
|
||||
// INVALID_TAGS_LENGTH(806, 400),
|
||||
// INVALID_CREATION_BYTES(809, 400),
|
||||
|
||||
// BLOG/Namestorage
|
||||
BODY_EMPTY(901, 400),
|
||||
BLOG_DISABLED(902, 403),
|
||||
NAME_NOT_OWNER(903, 422),
|
||||
TX_AMOUNT(904, 400),
|
||||
BLOG_ENTRY_NO_EXISTS(905, 404),
|
||||
BLOG_EMPTY(906, 404),
|
||||
POSTID_EMPTY(907, 400),
|
||||
POST_NOT_EXISTING(908, 404),
|
||||
COMMENTING_DISABLED(909, 403),
|
||||
COMMENT_NOT_EXISTING(910, 404),
|
||||
INVALID_COMMENT_OWNER(911, 422),
|
||||
// BODY_EMPTY(901, 400),
|
||||
// BLOG_DISABLED(902, 403),
|
||||
// NAME_NOT_OWNER(903, 422),
|
||||
// TX_AMOUNT(904, 400),
|
||||
// BLOG_ENTRY_NO_EXISTS(905, 404),
|
||||
// BLOG_EMPTY(906, 404),
|
||||
// POSTID_EMPTY(907, 400),
|
||||
// POST_NOT_EXISTING(908, 404),
|
||||
// COMMENTING_DISABLED(909, 403),
|
||||
// COMMENT_NOT_EXISTING(910, 404),
|
||||
// INVALID_COMMENT_OWNER(911, 422),
|
||||
|
||||
// Messages
|
||||
MESSAGE_FORMAT_NOT_HEX(1001, 400),
|
||||
MESSAGE_BLANK(1002, 400),
|
||||
NO_PUBLIC_KEY(1003, 422),
|
||||
MESSAGESIZE_EXCEEDED(1004, 400),
|
||||
// MESSAGE_FORMAT_NOT_HEX(1001, 400),
|
||||
// MESSAGE_BLANK(1002, 400),
|
||||
// NO_PUBLIC_KEY(1003, 422),
|
||||
// MESSAGESIZE_EXCEEDED(1004, 400),
|
||||
|
||||
// Groups
|
||||
GROUP_UNKNOWN(1101, 404);
|
||||
|
@@ -32,10 +32,13 @@ public class BlockMinterSummary {
|
||||
}
|
||||
|
||||
/** Constructs BlockMinterSummary in reward-share context. */
|
||||
public BlockMinterSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String recipientAccount) {
|
||||
this(mintingAccountPublicKey, blockCount);
|
||||
|
||||
public BlockMinterSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String minterAccount, String recipientAccount) {
|
||||
this.rewardSharePublicKey = rewardSharePublicKey;
|
||||
this.blockCount = blockCount;
|
||||
|
||||
this.mintingAccountPublicKey = mintingAccountPublicKey;
|
||||
this.mintingAccount = minterAccount;
|
||||
|
||||
this.recipientAccount = recipientAccount;
|
||||
}
|
||||
|
||||
|
15
src/main/java/org/qortal/api/model/NodeStatus.java
Normal file
15
src/main/java/org/qortal/api/model/NodeStatus.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class NodeStatus {
|
||||
|
||||
public boolean isMintingPossible;
|
||||
public boolean isSynchronizing;
|
||||
|
||||
public NodeStatus() {
|
||||
}
|
||||
|
||||
}
|
@@ -36,8 +36,8 @@ import javax.ws.rs.core.MediaType;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.appender.RollingFileAppender;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
@@ -45,6 +45,7 @@ import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.ActivitySummary;
|
||||
import org.qortal.api.model.NodeInfo;
|
||||
import org.qortal.api.model.NodeStatus;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
@@ -120,6 +121,27 @@ public class AdminResource {
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/status")
|
||||
@Operation(
|
||||
summary = "Fetch node status",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = NodeStatus.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
public NodeStatus status() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
NodeStatus nodeStatus = new NodeStatus();
|
||||
|
||||
nodeStatus.isMintingPossible = Controller.getInstance().isMintingPossible();
|
||||
nodeStatus.isSynchronizing = Controller.getInstance().isSynchronizing();
|
||||
|
||||
return nodeStatus;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/stop")
|
||||
@Operation(
|
||||
@@ -202,6 +224,8 @@ public class AdminResource {
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<MintingAccountData> getMintingAccounts() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<MintingAccountData> mintingAccounts = repository.getAccountRepository().getMintingAccounts();
|
||||
|
||||
@@ -216,7 +240,7 @@ public class AdminResource {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return new MintingAccountData(mintingAccountData.getPrivateKey(), rewardShareData);
|
||||
return new MintingAccountData(mintingAccountData, rewardShareData);
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return mintingAccounts;
|
||||
@@ -246,6 +270,8 @@ public class AdminResource {
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
|
||||
public String addMintingAccount(String seed58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] seed = Base58.decode(seed58.trim());
|
||||
|
||||
@@ -258,11 +284,11 @@ public class AdminResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
// Qortal: check reward-share's minting account is still allowed to mint
|
||||
PublicKeyAccount rewardShareMintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
|
||||
Account rewardShareMintingAccount = new Account(repository, rewardShareData.getMinter());
|
||||
if (!rewardShareMintingAccount.canMint())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.CANNOT_MINT);
|
||||
|
||||
MintingAccountData mintingAccountData = new MintingAccountData(seed);
|
||||
MintingAccountData mintingAccountData = new MintingAccountData(mintingAccount.getPrivateKey(), mintingAccount.getPublicKey());
|
||||
|
||||
repository.getAccountRepository().save(mintingAccountData);
|
||||
repository.saveChanges();
|
||||
@@ -296,6 +322,8 @@ public class AdminResource {
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
|
||||
public String deleteMintingAccount(String seed58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] seed = Base58.decode(seed58.trim());
|
||||
|
||||
|
@@ -410,7 +410,7 @@ public class AssetsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_ORDER_ID, ApiError.ORDER_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public OrderData getAssetOrder(@PathParam("orderid") String orderId58) {
|
||||
// Decode orderID
|
||||
@@ -424,7 +424,7 @@ public class AssetsResource {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
OrderData orderData = repository.getAssetRepository().fromOrderId(orderId);
|
||||
if (orderData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_UNKNOWN);
|
||||
|
||||
return orderData;
|
||||
} catch (DataException e) {
|
||||
@@ -451,7 +451,7 @@ public class AssetsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_ORDER_ID, ApiError.ORDER_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<TradeData> getAssetOrderTrades(@PathParam("orderid") String orderId58, @Parameter(
|
||||
ref = "limit"
|
||||
@@ -471,7 +471,7 @@ public class AssetsResource {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
OrderData orderData = repository.getAssetRepository().fromOrderId(orderId);
|
||||
if (orderData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_UNKNOWN);
|
||||
|
||||
return repository.getAssetRepository().getOrdersTrades(orderId, limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
@@ -497,7 +497,7 @@ public class AssetsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<OrderData> getAccountOrders(@PathParam("address") String address, @QueryParam("includeClosed") boolean includeClosed,
|
||||
@QueryParam("includeFulfilled") boolean includeFulfilled, @Parameter(
|
||||
@@ -514,11 +514,11 @@ public class AssetsResource {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
byte[] publicKey = accountData.getPublicKey();
|
||||
if (publicKey == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
return repository.getAssetRepository().getAccountsOrders(publicKey, includeClosed, includeFulfilled, limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
@@ -546,7 +546,7 @@ public class AssetsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<OrderData> getAccountAssetPairOrders(@PathParam("address") String address, @PathParam("assetid") int assetId,
|
||||
@PathParam("otherassetid") int otherAssetId, @QueryParam("isClosed") Boolean isClosed, @QueryParam("isFulfilled") Boolean isFulfilled, @Parameter(
|
||||
@@ -563,11 +563,11 @@ public class AssetsResource {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
byte[] publicKey = accountData.getPublicKey();
|
||||
if (publicKey == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
if (!repository.getAssetRepository().assetExists(assetId))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
|
||||
|
@@ -59,7 +59,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getBlock(@PathParam("signature") String signature58) {
|
||||
// Decode signature
|
||||
@@ -98,7 +98,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(
|
||||
ref = "limit"
|
||||
@@ -117,7 +117,7 @@ public class BlocksResource {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
@@ -144,13 +144,11 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getFirstBlock() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().fromHeight(1);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -173,13 +171,11 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getLastBlock() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().getLastBlock();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -202,7 +198,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getChild(@PathParam("signature") String signature58) {
|
||||
// Decode signature
|
||||
@@ -218,13 +214,13 @@ public class BlocksResource {
|
||||
|
||||
// Check block exists
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
BlockData childBlockData = repository.getBlockRepository().fromReference(signature);
|
||||
|
||||
// Check child block exists
|
||||
if (childBlockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return childBlockData;
|
||||
} catch (ApiException e) {
|
||||
@@ -282,7 +278,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public int getHeight(@PathParam("signature") String signature58) {
|
||||
// Decode signature
|
||||
@@ -298,7 +294,7 @@ public class BlocksResource {
|
||||
|
||||
// Check block exists
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return blockData.getHeight();
|
||||
} catch (ApiException e) {
|
||||
@@ -325,13 +321,13 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getByHeight(@PathParam("height") int height) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return blockData;
|
||||
} catch (ApiException e) {
|
||||
@@ -357,17 +353,17 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getByTimestamp(@PathParam("timestamp") long timestamp) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int height = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
|
||||
if (height == 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return blockData;
|
||||
} catch (ApiException e) {
|
||||
@@ -396,7 +392,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<BlockData> getBlockRange(@PathParam("height") int height, @Parameter(
|
||||
ref = "count"
|
||||
@@ -414,8 +410,6 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
return blocks;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ 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.NameSummary;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@@ -122,10 +123,17 @@ public class NamesResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public NameData getName(@PathParam("name") String name) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getNameRepository().fromName(name);
|
||||
NameData nameData = repository.getNameRepository().fromName(name);
|
||||
|
||||
if (nameData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NAME_UNKNOWN);
|
||||
|
||||
return nameData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
@@ -31,6 +31,7 @@ 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;
|
||||
|
||||
@Path("/peers")
|
||||
@Tag(name = "Peers")
|
||||
@@ -108,6 +109,29 @@ public class PeersResource {
|
||||
return Network.getInstance().getSelfPeers();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/enginestats")
|
||||
@Operation(
|
||||
summary = "Fetch statistics snapshot for networking engine",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = ExecuteProduceConsume.StatsSnapshot.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public ExecuteProduceConsume.StatsSnapshot getEngineStats() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
return Network.getInstance().getStatsSnapshot();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Operation(
|
||||
summary = "Add new peer address",
|
||||
@@ -137,7 +161,7 @@ public class PeersResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public String addPeer(String address) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
@@ -151,7 +175,7 @@ public class PeersResource {
|
||||
|
||||
return "true";
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_NETWORK_ADDRESS);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
@@ -188,7 +212,7 @@ public class PeersResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public String removePeer(String address) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
@@ -199,7 +223,7 @@ public class PeersResource {
|
||||
boolean wasKnown = Network.getInstance().forgetPeer(peerAddress);
|
||||
return wasKnown ? "true" : "false";
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_NETWORK_ADDRESS);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
@@ -223,7 +247,7 @@ public class PeersResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public String removeKnownPeers(String address) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
@@ -232,8 +256,6 @@ public class PeersResource {
|
||||
int numDeleted = Network.getInstance().forgetAllPeers();
|
||||
|
||||
return numDeleted != 0 ? "true" : "false";
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
@@ -69,9 +69,9 @@ public class TransactionsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public TransactionData getTransaction(@PathParam("signature") String signature58) {
|
||||
public TransactionData getTransactionBySignature(@PathParam("signature") String signature58) {
|
||||
byte[] signature;
|
||||
try {
|
||||
signature = Base58.decode(signature58);
|
||||
@@ -82,7 +82,7 @@ public class TransactionsResource {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_UNKNOWN);
|
||||
|
||||
return transactionData;
|
||||
} catch (ApiException e) {
|
||||
@@ -110,9 +110,9 @@ public class TransactionsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE, ApiError.TRANSFORMATION_ERROR
|
||||
ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_UNKNOWN, ApiError.REPOSITORY_ISSUE, ApiError.TRANSFORMATION_ERROR
|
||||
})
|
||||
public String getRawTransaction(@PathParam("signature") String signature58) {
|
||||
public String getRawTransactionBySignature(@PathParam("signature") String signature58) {
|
||||
byte[] signature;
|
||||
try {
|
||||
signature = Base58.decode(signature58);
|
||||
@@ -123,7 +123,7 @@ public class TransactionsResource {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_UNKNOWN);
|
||||
|
||||
byte[] transactionBytes = TransactionTransformer.toBytes(transactionData);
|
||||
|
||||
@@ -137,6 +137,46 @@ public class TransactionsResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/reference/{reference}")
|
||||
@Operation(
|
||||
summary = "Fetch transaction using transaction reference",
|
||||
description = "Returns transaction",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "a transaction",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
implementation = TransactionData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_REFERENCE, ApiError.TRANSACTION_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public TransactionData getTransactionByReference(@PathParam("reference") String reference58) {
|
||||
byte[] reference;
|
||||
try {
|
||||
reference = Base58.decode(reference58);
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE, e);
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromReference(reference);
|
||||
if (transactionData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_UNKNOWN);
|
||||
|
||||
return transactionData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/block/{signature}")
|
||||
@Operation(
|
||||
@@ -156,7 +196,7 @@ public class TransactionsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(
|
||||
ref = "limit"
|
||||
@@ -175,7 +215,7 @@ public class TransactionsResource {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
@@ -430,7 +470,7 @@ public class TransactionsResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
|
||||
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (!blockchainLock.tryLock(500, TimeUnit.MILLISECONDS))
|
||||
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS))
|
||||
throw createTransactionInvalidException(request, ValidationResult.NO_BLOCKCHAIN_LOCK);
|
||||
|
||||
try {
|
||||
@@ -533,7 +573,7 @@ public class TransactionsResource {
|
||||
|
||||
public static ApiException createTransactionInvalidException(HttpServletRequest request, ValidationResult result) {
|
||||
String translatedResult = Translator.INSTANCE.translate("TransactionValidity", request.getLocale().getLanguage(), result.name());
|
||||
return ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID, null, translatedResult);
|
||||
return ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID, null, translatedResult, result.name());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@ import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
@@ -126,31 +125,43 @@ public class Block {
|
||||
protected BigDecimal ourAtFees; // Generated locally
|
||||
|
||||
/** Lazy-instantiated expanded info on block's online accounts. */
|
||||
class ExpandedAccount {
|
||||
final RewardShareData rewardShareData;
|
||||
final boolean isRecipientAlsoMinter;
|
||||
static class ExpandedAccount {
|
||||
private static final BigDecimal oneHundred = BigDecimal.valueOf(100L);
|
||||
|
||||
final Account mintingAccount;
|
||||
final AccountData mintingAccountData;
|
||||
final boolean isMinterFounder;
|
||||
private final Repository repository;
|
||||
|
||||
final Account recipientAccount;
|
||||
final AccountData recipientAccountData;
|
||||
final boolean isRecipientFounder;
|
||||
private final RewardShareData rewardShareData;
|
||||
private final boolean isRecipientAlsoMinter;
|
||||
|
||||
private final Account mintingAccount;
|
||||
private final AccountData mintingAccountData;
|
||||
private final boolean isMinterFounder;
|
||||
|
||||
private final Account recipientAccount;
|
||||
private final AccountData recipientAccountData;
|
||||
private final boolean isRecipientFounder;
|
||||
|
||||
ExpandedAccount(Repository repository, int accountIndex) throws DataException {
|
||||
this.repository = repository;
|
||||
this.rewardShareData = repository.getAccountRepository().getRewardShareByIndex(accountIndex);
|
||||
|
||||
this.mintingAccount = new PublicKeyAccount(repository, this.rewardShareData.getMinterPublicKey());
|
||||
this.recipientAccount = new Account(repository, this.rewardShareData.getRecipient());
|
||||
|
||||
this.mintingAccount = new Account(repository, this.rewardShareData.getMinter());
|
||||
this.mintingAccountData = repository.getAccountRepository().getAccount(this.mintingAccount.getAddress());
|
||||
this.isMinterFounder = Account.isFounder(mintingAccountData.getFlags());
|
||||
|
||||
this.recipientAccountData = repository.getAccountRepository().getAccount(this.recipientAccount.getAddress());
|
||||
this.isRecipientFounder = Account.isFounder(recipientAccountData.getFlags());
|
||||
this.isRecipientAlsoMinter = this.rewardShareData.getRecipient().equals(this.mintingAccount.getAddress());
|
||||
|
||||
this.isRecipientAlsoMinter = this.mintingAccountData.getAddress().equals(this.recipientAccountData.getAddress());
|
||||
if (this.isRecipientAlsoMinter) {
|
||||
// Self-share: minter is also recipient
|
||||
this.recipientAccount = this.mintingAccount;
|
||||
this.recipientAccountData = this.mintingAccountData;
|
||||
this.isRecipientFounder = this.isMinterFounder;
|
||||
} else {
|
||||
// Recipient differs from minter
|
||||
this.recipientAccount = new Account(repository, this.rewardShareData.getRecipient());
|
||||
this.recipientAccountData = repository.getAccountRepository().getAccount(this.recipientAccount.getAddress());
|
||||
this.isRecipientFounder = Account.isFounder(recipientAccountData.getFlags());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,22 +187,26 @@ public class Block {
|
||||
}
|
||||
|
||||
void distribute(BigDecimal accountAmount) throws DataException {
|
||||
final BigDecimal oneHundred = BigDecimal.valueOf(100L);
|
||||
|
||||
if (this.mintingAccount.getAddress().equals(this.recipientAccount.getAddress())) {
|
||||
if (this.isRecipientAlsoMinter) {
|
||||
// minter & recipient the same - simpler case
|
||||
LOGGER.trace(() -> String.format("Minter/recipient account %s share: %s", this.mintingAccount.getAddress(), accountAmount.toPlainString()));
|
||||
this.mintingAccount.setConfirmedBalance(Asset.QORT, this.mintingAccount.getConfirmedBalance(Asset.QORT).add(accountAmount));
|
||||
if (accountAmount.signum() != 0)
|
||||
// this.mintingAccount.setConfirmedBalance(Asset.QORT, this.mintingAccount.getConfirmedBalance(Asset.QORT).add(accountAmount));
|
||||
this.repository.getAccountRepository().modifyAssetBalance(this.mintingAccount.getAddress(), Asset.QORT, accountAmount);
|
||||
} else {
|
||||
// minter & recipient different - extra work needed
|
||||
BigDecimal recipientAmount = accountAmount.multiply(this.rewardShareData.getSharePercent()).divide(oneHundred, RoundingMode.DOWN);
|
||||
BigDecimal minterAmount = accountAmount.subtract(recipientAmount);
|
||||
|
||||
LOGGER.trace(() -> String.format("Minter account %s share: %s", this.mintingAccount.getAddress(), minterAmount.toPlainString()));
|
||||
this.mintingAccount.setConfirmedBalance(Asset.QORT, this.mintingAccount.getConfirmedBalance(Asset.QORT).add(minterAmount));
|
||||
if (minterAmount.signum() != 0)
|
||||
// this.mintingAccount.setConfirmedBalance(Asset.QORT, this.mintingAccount.getConfirmedBalance(Asset.QORT).add(minterAmount));
|
||||
this.repository.getAccountRepository().modifyAssetBalance(this.mintingAccount.getAddress(), Asset.QORT, minterAmount);
|
||||
|
||||
LOGGER.trace(() -> String.format("Recipient account %s share: %s", this.recipientAccount.getAddress(), recipientAmount.toPlainString()));
|
||||
this.recipientAccount.setConfirmedBalance(Asset.QORT, this.recipientAccount.getConfirmedBalance(Asset.QORT).add(recipientAmount));
|
||||
if (recipientAmount.signum() != 0)
|
||||
// this.recipientAccount.setConfirmedBalance(Asset.QORT, this.recipientAccount.getConfirmedBalance(Asset.QORT).add(recipientAmount));
|
||||
this.repository.getAccountRepository().modifyAssetBalance(this.recipientAccount.getAddress(), Asset.QORT, recipientAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1256,8 +1271,9 @@ public class Block {
|
||||
AccountData accountData = getAccountData.apply(expandedAccount);
|
||||
|
||||
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
||||
repository.getAccountRepository().setMintedBlockCount(accountData);
|
||||
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||
// repository.getAccountRepository().setMintedBlockCount(accountData); int rowCount = 1; // Until HSQLDB rev 6100 is fixed
|
||||
int rowCount = repository.getAccountRepository().modifyMintedBlockCount(accountData.getAddress(), +1);
|
||||
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s (rowCount: %d)", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : ""), rowCount));
|
||||
}
|
||||
|
||||
// We are only interested in accounts that are NOT already highest level
|
||||
@@ -1425,35 +1441,40 @@ public class Block {
|
||||
public void orphan() throws DataException {
|
||||
LOGGER.trace(() -> String.format("Orphaning block %d", this.blockData.getHeight()));
|
||||
|
||||
// Return AT fees and delete AT states from repository
|
||||
orphanAtFeesAndStates();
|
||||
this.repository.setDebug(false);
|
||||
try {
|
||||
// Return AT fees and delete AT states from repository
|
||||
orphanAtFeesAndStates();
|
||||
|
||||
// Orphan, and unlink, transactions from this block
|
||||
orphanTransactionsFromBlock();
|
||||
// Orphan, and unlink, transactions from this block
|
||||
orphanTransactionsFromBlock();
|
||||
|
||||
// Undo any group-approval decisions that happen at this block
|
||||
orphanGroupApprovalTransactions();
|
||||
// Undo any group-approval decisions that happen at this block
|
||||
orphanGroupApprovalTransactions();
|
||||
|
||||
if (this.blockData.getHeight() > 1) {
|
||||
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
|
||||
this.cachedExpandedAccounts = null;
|
||||
if (this.blockData.getHeight() > 1) {
|
||||
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
|
||||
this.cachedExpandedAccounts = null;
|
||||
|
||||
// Deduct any transaction fees from minter/reward-share account(s)
|
||||
deductTransactionFees();
|
||||
// Deduct any transaction fees from minter/reward-share account(s)
|
||||
deductTransactionFees();
|
||||
|
||||
// Block rewards removed after transactions undone
|
||||
orphanBlockRewards();
|
||||
// Block rewards removed after transactions undone
|
||||
orphanBlockRewards();
|
||||
|
||||
// Decrease account levels
|
||||
decreaseAccountLevels();
|
||||
// Decrease account levels
|
||||
decreaseAccountLevels();
|
||||
}
|
||||
|
||||
// Delete orphaned balances
|
||||
this.repository.getAccountRepository().deleteBalancesFromHeight(this.blockData.getHeight());
|
||||
|
||||
// Delete block from blockchain
|
||||
this.repository.getBlockRepository().delete(this.blockData);
|
||||
this.blockData.setHeight(null);
|
||||
} finally {
|
||||
this.repository.setDebug(false);
|
||||
}
|
||||
|
||||
// Delete orphaned balances
|
||||
this.repository.getAccountRepository().deleteBalancesFromHeight(this.blockData.getHeight());
|
||||
|
||||
// Delete block from blockchain
|
||||
this.repository.getBlockRepository().delete(this.blockData);
|
||||
this.blockData.setHeight(null);
|
||||
}
|
||||
|
||||
protected void orphanTransactionsFromBlock() throws DataException {
|
||||
@@ -1571,8 +1592,9 @@ public class Block {
|
||||
AccountData accountData = getAccountData.apply(expandedAccount);
|
||||
|
||||
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
|
||||
repository.getAccountRepository().setMintedBlockCount(accountData);
|
||||
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||
// repository.getAccountRepository().setMintedBlockCount(accountData); int rowCount = 1; // Until HSQLDB rev 6100 is fixed
|
||||
int rowCount = repository.getAccountRepository().modifyMintedBlockCount(accountData.getAddress(), -1);
|
||||
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s (rowCount: %d)", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : ""), rowCount));
|
||||
}
|
||||
|
||||
// We are only interested in accounts that are NOT already lowest level
|
||||
@@ -1602,8 +1624,22 @@ public class Block {
|
||||
protected void distributeBlockReward(BigDecimal totalAmount) throws DataException {
|
||||
LOGGER.trace(() -> String.format("Distributing: %s", totalAmount.toPlainString()));
|
||||
|
||||
List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
|
||||
// Distribute according to account level
|
||||
BigDecimal sharedByLevelAmount = distributeBlockRewardByLevel(totalAmount);
|
||||
LOGGER.trace(() -> String.format("Shared %s of %s based on account levels", sharedByLevelAmount.toPlainString(), totalAmount.toPlainString()));
|
||||
|
||||
// Distribute amongst legacy QORA holders
|
||||
BigDecimal sharedByQoraHoldersAmount = distributeBlockRewardToQoraHolders(totalAmount);
|
||||
LOGGER.trace(() -> String.format("Shared %s of %s to legacy QORA holders", sharedByQoraHoldersAmount.toPlainString(), totalAmount.toPlainString()));
|
||||
|
||||
// Spread remainder across founder accounts
|
||||
BigDecimal foundersAmount = totalAmount.subtract(sharedByLevelAmount).subtract(sharedByQoraHoldersAmount);
|
||||
distributeBlockRewardToFounders(foundersAmount);
|
||||
}
|
||||
|
||||
private BigDecimal distributeBlockRewardByLevel(BigDecimal totalAmount) throws DataException {
|
||||
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
List<ShareByLevel> sharesByLevel = BlockChain.getInstance().getBlockSharesByLevel();
|
||||
|
||||
// Distribute amount across bins
|
||||
BigDecimal sharedAmount = BigDecimal.ZERO;
|
||||
@@ -1628,36 +1664,17 @@ public class Block {
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute share across legacy QORA holders
|
||||
return sharedAmount;
|
||||
}
|
||||
|
||||
private BigDecimal distributeBlockRewardToQoraHolders(BigDecimal totalAmount) throws DataException {
|
||||
BigDecimal qoraHoldersAmount = BlockChain.getInstance().getQoraHoldersShare().multiply(totalAmount).setScale(8, RoundingMode.DOWN);
|
||||
LOGGER.trace(() -> String.format("Legacy QORA holders share of %s: %s", totalAmount.toPlainString(), qoraHoldersAmount.toPlainString()));
|
||||
|
||||
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getAssetBalances(Asset.LEGACY_QORA, true);
|
||||
final boolean isProcessingNotOrphaning = totalAmount.signum() >= 0;
|
||||
|
||||
// Filter out qoraHolders who have received max QORT due to holding legacy QORA, (ratio from blockchain config)
|
||||
BigDecimal qoraPerQortReward = BlockChain.getInstance().getQoraPerQortReward();
|
||||
Iterator<AccountBalanceData> qoraHoldersIterator = qoraHolders.iterator();
|
||||
while (qoraHoldersIterator.hasNext()) {
|
||||
AccountBalanceData qoraHolder = qoraHoldersIterator.next();
|
||||
|
||||
Account qoraHolderAccount = new Account(repository, qoraHolder.getAddress());
|
||||
BigDecimal qortFromQora = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA);
|
||||
|
||||
// If we're processing a block, then totalAmount will be positive
|
||||
if (totalAmount.signum() >= 0) {
|
||||
BigDecimal maxQortFromQora = qoraHolder.getBalance().divide(qoraPerQortReward, RoundingMode.DOWN);
|
||||
|
||||
// Disregard qora holders who have already received maximum qort from holding legacy qora
|
||||
if (qortFromQora.compareTo(maxQortFromQora) >= 0)
|
||||
qoraHoldersIterator.remove();
|
||||
} else {
|
||||
// We're orphaning a block
|
||||
// so disregard qora holders who have already had their final qort-from-qora reward (i.e. reward reward block is earlier than this one)
|
||||
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
||||
if (qortFromQoraData != null && qortFromQoraData.getFinalBlockHeight() < this.blockData.getHeight())
|
||||
qoraHoldersIterator.remove();
|
||||
}
|
||||
}
|
||||
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
|
||||
|
||||
BigDecimal totalQoraHeld = BigDecimal.ZERO;
|
||||
for (int i = 0; i < qoraHolders.size(); ++i)
|
||||
@@ -1666,6 +1683,7 @@ public class Block {
|
||||
BigDecimal finalTotalQoraHeld = totalQoraHeld;
|
||||
LOGGER.trace(() -> String.format("Total legacy QORA held: %s", finalTotalQoraHeld.toPlainString()));
|
||||
|
||||
BigDecimal sharedAmount = BigDecimal.ZERO;
|
||||
for (int h = 0; h < qoraHolders.size(); ++h) {
|
||||
AccountBalanceData qoraHolder = qoraHolders.get(h);
|
||||
|
||||
@@ -1674,12 +1692,16 @@ public class Block {
|
||||
LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s",
|
||||
qoraHolder.getAddress(), qoraHolder.getBalance().toPlainString(), finalTotalQoraHeld, finalHolderReward.toPlainString()));
|
||||
|
||||
// Too small to register this time?
|
||||
if (holderReward.signum() == 0)
|
||||
continue;
|
||||
|
||||
Account qoraHolderAccount = new Account(repository, qoraHolder.getAddress());
|
||||
|
||||
BigDecimal newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA).add(holderReward);
|
||||
|
||||
// If processing, make sure we don't overpay
|
||||
if (totalAmount.signum() >= 0) {
|
||||
if (isProcessingNotOrphaning) {
|
||||
BigDecimal maxQortFromQora = qoraHolder.getBalance().divide(qoraPerQortReward, RoundingMode.DOWN);
|
||||
|
||||
if (newQortFromQoraBalance.compareTo(maxQortFromQora) >= 0) {
|
||||
@@ -1689,7 +1711,7 @@ public class Block {
|
||||
holderReward = holderReward.subtract(adjustment);
|
||||
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
|
||||
|
||||
// This is also qora holders final qort-from-qora block
|
||||
// This is also the QORA holder's final QORT-from-QORA block
|
||||
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), holderReward, this.blockData.getHeight());
|
||||
this.repository.getAccountRepository().save(qortFromQoraData);
|
||||
|
||||
@@ -1701,9 +1723,10 @@ public class Block {
|
||||
// Orphaning
|
||||
QortFromQoraData qortFromQoraData = this.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
|
||||
if (qortFromQoraData != null) {
|
||||
// Note use of negate() here as qortFromQora will be negative during orphaning,
|
||||
// but final qort-from-qora is stored in repository during processing (and hence positive).
|
||||
BigDecimal adjustment = holderReward.subtract(qortFromQoraData.getFinalQortFromQora().negate());
|
||||
// Final QORT-from-QORA amount from repository was stored during processing, and hence positive.
|
||||
// So we use add() here as qortFromQora is negative during orphaning.
|
||||
// More efficient than holderReward.subtract(final-qort-from-qora.negate())
|
||||
BigDecimal adjustment = holderReward.add(qortFromQoraData.getFinalQortFromQora());
|
||||
|
||||
holderReward = holderReward.subtract(adjustment);
|
||||
newQortFromQoraBalance = newQortFromQoraBalance.subtract(adjustment);
|
||||
@@ -1716,7 +1739,8 @@ public class Block {
|
||||
}
|
||||
}
|
||||
|
||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(holderReward));
|
||||
// qoraHolderAccount.setConfirmedBalance(Asset.QORT, qoraHolderAccount.getConfirmedBalance(Asset.QORT).add(holderReward));
|
||||
this.repository.getAccountRepository().modifyAssetBalance(qoraHolder.getAddress(), Asset.QORT, holderReward);
|
||||
|
||||
if (newQortFromQoraBalance.signum() > 0)
|
||||
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
|
||||
@@ -1727,27 +1751,39 @@ public class Block {
|
||||
sharedAmount = sharedAmount.add(holderReward);
|
||||
}
|
||||
|
||||
// Spread remainder across founder accounts
|
||||
BigDecimal foundersAmount = totalAmount.subtract(sharedAmount);
|
||||
BigDecimal finalSharedAmount = sharedAmount;
|
||||
return sharedAmount;
|
||||
}
|
||||
|
||||
private void distributeBlockRewardToFounders(BigDecimal foundersAmount) throws DataException {
|
||||
// Remaining reward portion is spread across all founders, online or not
|
||||
List<AccountData> founderAccounts = this.repository.getAccountRepository().getFlaggedAccounts(Account.FOUNDER_FLAG);
|
||||
BigDecimal foundersCount = BigDecimal.valueOf(founderAccounts.size());
|
||||
BigDecimal perFounderAmount = foundersAmount.divide(foundersCount, RoundingMode.DOWN);
|
||||
|
||||
LOGGER.trace(() -> String.format("Shared %s of %s, remaining %s to %d founder%s, %s each",
|
||||
finalSharedAmount.toPlainString(), totalAmount.toPlainString(),
|
||||
LOGGER.trace(() -> String.format("Sharing remaining %s to %d founder%s, %s each",
|
||||
foundersAmount.toPlainString(), founderAccounts.size(), (founderAccounts.size() != 1 ? "s" : ""),
|
||||
perFounderAmount.toPlainString()));
|
||||
|
||||
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
for (int a = 0; a < founderAccounts.size(); ++a) {
|
||||
Account founderAccount = new Account(this.repository, founderAccounts.get(a).getAddress());
|
||||
|
||||
// If founder is minter in any online reward-shares then founder's amount is spread across these, otherwise founder gets whole amount.
|
||||
|
||||
/* Fixed version:
|
||||
List<ExpandedAccount> founderExpandedAccounts = expandedAccounts.stream().filter(
|
||||
accountInfo -> accountInfo.isMinterFounder &&
|
||||
accountInfo.mintingAccountData.getAddress().equals(founderAccount.getAddress())
|
||||
).collect(Collectors.toList());
|
||||
*/
|
||||
|
||||
// Broken version:
|
||||
List<ExpandedAccount> founderExpandedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.isMinterFounder).collect(Collectors.toList());
|
||||
|
||||
if (founderExpandedAccounts.isEmpty()) {
|
||||
// Simple case: no founder-as-minter reward-shares online so founder gets whole amount.
|
||||
Account founderAccount = new Account(this.repository, founderAccounts.get(a).getAddress());
|
||||
founderAccount.setConfirmedBalance(Asset.QORT, founderAccount.getConfirmedBalance(Asset.QORT).add(perFounderAmount));
|
||||
// founderAccount.setConfirmedBalance(Asset.QORT, founderAccount.getConfirmedBalance(Asset.QORT).add(perFounderAmount));
|
||||
this.repository.getAccountRepository().modifyAssetBalance(founderAccount.getAddress(), Asset.QORT, perFounderAmount);
|
||||
} else {
|
||||
// Distribute over reward-shares
|
||||
BigDecimal perFounderRewardShareAmount = perFounderAmount.divide(BigDecimal.valueOf(founderExpandedAccounts.size()), RoundingMode.DOWN);
|
||||
|
@@ -12,7 +12,6 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.block.Block.ValidationResult;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
@@ -123,7 +122,7 @@ public class BlockMinter extends Thread {
|
||||
continue;
|
||||
}
|
||||
|
||||
PublicKeyAccount mintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
|
||||
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
|
||||
if (!mintingAccount.canMint()) {
|
||||
// Minting-account component of reward-share can no longer mint - disregard
|
||||
madi.remove();
|
||||
@@ -137,19 +136,18 @@ public class BlockMinter extends Thread {
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(Controller.hasMisbehaved);
|
||||
|
||||
// Don't mint if we don't have enough connected peers as where would the transactions/consensus come from?
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
continue;
|
||||
|
||||
// Disregard peers that don't have a recent block
|
||||
peers.removeIf(Controller.hasNoRecentBlock);
|
||||
|
||||
// If we have any peers with a recent block, but our latest block isn't recent
|
||||
// then we need to synchronize instead of minting.
|
||||
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
continue;
|
||||
|
||||
// If our latest block isn't recent then we need to synchronize instead of minting.
|
||||
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||
continue;
|
||||
|
||||
// There are no peers with a recent block and/or our latest block is recent
|
||||
// There are enough peers with a recent block and our latest block is recent
|
||||
// so go ahead and mint a block if possible.
|
||||
isMintingPossible = true;
|
||||
|
||||
@@ -159,12 +157,12 @@ public class BlockMinter extends Thread {
|
||||
newBlocks.clear();
|
||||
}
|
||||
|
||||
// Discard accounts we have already built blocks with
|
||||
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
|
||||
|
||||
// Do we need to build any potential new blocks?
|
||||
List<PrivateKeyAccount> mintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
|
||||
|
||||
// Discard accounts we have blocks for
|
||||
mintingAccounts.removeIf(account -> newBlocks.stream().anyMatch(newBlock -> newBlock.getMinter().getAddress().equals(account.getAddress())));
|
||||
|
||||
for (PrivateKeyAccount mintingAccount : mintingAccounts) {
|
||||
// First block does the AT heavy-lifting
|
||||
if (newBlocks.isEmpty()) {
|
||||
@@ -258,11 +256,10 @@ public class BlockMinter extends Thread {
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
|
||||
|
||||
if (rewardShareData != null) {
|
||||
PublicKeyAccount mintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
|
||||
LOGGER.info(String.format("Minted block %d, sig %.8s by %s on behalf of %s",
|
||||
newBlock.getBlockData().getHeight(),
|
||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
||||
mintingAccount.getAddress(),
|
||||
rewardShareData.getMinter(),
|
||||
rewardShareData.getRecipient()));
|
||||
} else {
|
||||
LOGGER.info(String.format("Minted block %d, sig %.8s by %s",
|
||||
@@ -341,17 +338,19 @@ public class BlockMinter extends Thread {
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
public static void mintTestingBlock(Repository repository, PrivateKeyAccount mintingAccount) throws DataException {
|
||||
public static void mintTestingBlock(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
|
||||
if (!BlockChain.getInstance().isTestChain()) {
|
||||
LOGGER.warn("Ignoring attempt to mint testing block for non-test chain!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure mintingAccount is 'online' so blocks can be minted
|
||||
Controller.getInstance().ensureTestingAccountOnline(mintingAccount);
|
||||
Controller.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
|
||||
|
||||
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
||||
|
||||
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
||||
|
||||
// Make sure we're the only thread modifying the blockchain
|
||||
|
@@ -21,6 +21,7 @@ import org.qortal.ApplyUpdate;
|
||||
import org.qortal.api.ApiRequest;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.globalization.Translator;
|
||||
import org.qortal.gui.SysTray;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -231,6 +232,10 @@ public class AutoUpdate extends Thread {
|
||||
// JVM arguments
|
||||
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
|
||||
|
||||
// Remove JNI options as they won't be supported by command-line 'java'
|
||||
// These are typically added by the AdvancedInstaller Java launcher EXE
|
||||
javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
|
||||
|
||||
// Call ApplyUpdate using new JAR
|
||||
javaCmd.addAll(Arrays.asList("-cp", NEW_JAR_FILENAME, ApplyUpdate.class.getCanonicalName()));
|
||||
|
||||
@@ -241,7 +246,9 @@ public class AutoUpdate extends Thread {
|
||||
|
||||
LOGGER.info(String.format("Applying update with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
SysTray.getInstance().showMessage("Auto Update", "Applying automatic update and restarting...", MessageType.INFO);
|
||||
SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "AUTO_UPDATE"),
|
||||
Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"),
|
||||
MessageType.INFO);
|
||||
|
||||
new ProcessBuilder(javaCmd).start();
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ package org.qortal.controller;
|
||||
import java.math.BigInteger;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -39,13 +39,13 @@ public class Synchronizer {
|
||||
|
||||
private static final int INITIAL_BLOCK_STEP = 8;
|
||||
private static final int MAXIMUM_BLOCK_STEP = 500;
|
||||
private static final int MAXIMUM_COMMON_DELTA = 1440; // XXX move to Settings?
|
||||
private static final int MAXIMUM_COMMON_DELTA = 240; // XXX move to Settings?
|
||||
private static final int SYNC_BATCH_SIZE = 200;
|
||||
|
||||
private static Synchronizer instance;
|
||||
|
||||
public enum SynchronizationResult {
|
||||
OK, NOTHING_TO_DO, GENESIS_ONLY, NO_COMMON_BLOCK, TOO_DIVERGENT, NO_REPLY, INFERIOR_CHAIN, INVALID_DATA, NO_BLOCKCHAIN_LOCK, REPOSITORY_ISSUE;
|
||||
OK, NOTHING_TO_DO, GENESIS_ONLY, NO_COMMON_BLOCK, TOO_DIVERGENT, NO_REPLY, INFERIOR_CHAIN, INVALID_DATA, NO_BLOCKCHAIN_LOCK, REPOSITORY_ISSUE, SHUTTING_DOWN;
|
||||
}
|
||||
|
||||
// Constructors
|
||||
@@ -93,15 +93,11 @@ public class Synchronizer {
|
||||
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
|
||||
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
||||
|
||||
List<BlockSummaryData> peerBlockSummaries = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight);
|
||||
if (peerBlockSummaries == null) {
|
||||
LOGGER.info(String.format("Error while trying to find common block with peer %s", peer));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
if (peerBlockSummaries.isEmpty()) {
|
||||
LOGGER.info(String.format("Failure to find common block with peer %s", peer));
|
||||
return SynchronizationResult.NO_COMMON_BLOCK;
|
||||
}
|
||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, force, peerBlockSummaries);
|
||||
if (findCommonBlockResult != SynchronizationResult.OK)
|
||||
// Logging performed by fetchSummariesFromCommonBlock() above
|
||||
return findCommonBlockResult;
|
||||
|
||||
// First summary is common block
|
||||
final BlockData commonBlockData = repository.getBlockRepository().fromSignature(peerBlockSummaries.get(0).getSignature());
|
||||
@@ -129,13 +125,6 @@ public class Synchronizer {
|
||||
return SynchronizationResult.NOTHING_TO_DO;
|
||||
}
|
||||
|
||||
// If common block is too far behind us then we're on massively different forks so give up.
|
||||
int minCommonHeight = ourInitialHeight - MAXIMUM_COMMON_DELTA;
|
||||
if (!force && commonBlockHeight < minCommonHeight) {
|
||||
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
|
||||
return SynchronizationResult.TOO_DIVERGENT;
|
||||
}
|
||||
|
||||
// Unless we're doing a forced sync, we might need to compare blocks after common block
|
||||
if (!force && ourInitialHeight > commonBlockHeight) {
|
||||
// If our latest block is very old, we're very behind and should ditch our fork.
|
||||
@@ -154,6 +143,9 @@ public class Synchronizer {
|
||||
int peerBlockCount = peerHeight - commonBlockHeight;
|
||||
|
||||
while (peerBlockSummaries.size() < peerBlockCount) {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
int lastSummaryHeight = commonBlockHeight + peerBlockSummaries.size();
|
||||
byte[] previousSignature;
|
||||
if (peerBlockSummaries.isEmpty())
|
||||
@@ -212,6 +204,9 @@ public class Synchronizer {
|
||||
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58));
|
||||
|
||||
while (ourHeight > commonBlockHeight) {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(ourHeight);
|
||||
Block block = new Block(repository, blockData);
|
||||
block.orphan();
|
||||
@@ -232,6 +227,9 @@ public class Synchronizer {
|
||||
List<byte[]> peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList());
|
||||
|
||||
while (ourHeight < peerHeight && ourHeight < maxBatchHeight) {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
// Do we need more signatures?
|
||||
if (peerBlockSignatures.isEmpty()) {
|
||||
int numberRequested = maxBatchHeight - ourHeight;
|
||||
@@ -320,45 +318,59 @@ public class Synchronizer {
|
||||
* @throws DataException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
private List<BlockSummaryData> fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight) throws DataException, InterruptedException {
|
||||
private SynchronizationResult fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight, boolean force, List<BlockSummaryData> blockSummariesFromCommon) throws DataException, InterruptedException {
|
||||
// Start by asking for a few recent block hashes as this will cover a majority of reorgs
|
||||
// Failing that, back off exponentially
|
||||
int step = INITIAL_BLOCK_STEP;
|
||||
|
||||
List<BlockSummaryData> blockSummaries = null;
|
||||
|
||||
int testHeight = Math.max(ourHeight - step, 1);
|
||||
BlockData testBlockData = null;
|
||||
|
||||
List<BlockSummaryData> blockSummariesBatch = null;
|
||||
|
||||
while (testHeight >= 1) {
|
||||
// Are we shutting down?
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
// Fetch our block signature at this height
|
||||
testBlockData = repository.getBlockRepository().fromHeight(testHeight);
|
||||
if (testBlockData == null) {
|
||||
// Not found? But we've locked the blockchain and height is below blockchain's tip!
|
||||
LOGGER.error("Failed to get block at height lower than blockchain tip during synchronization?");
|
||||
return null;
|
||||
return SynchronizationResult.REPOSITORY_ISSUE;
|
||||
}
|
||||
|
||||
// Ask for block signatures since test block's signature
|
||||
byte[] testSignature = testBlockData.getSignature();
|
||||
LOGGER.trace(String.format("Requesting %d summar%s after height %d", step, (step != 1 ? "ies": "y"), testHeight));
|
||||
blockSummaries = this.getBlockSummaries(peer, testSignature, step);
|
||||
blockSummariesBatch = this.getBlockSummaries(peer, testSignature, step);
|
||||
|
||||
if (blockSummaries == null)
|
||||
if (blockSummariesBatch == null) {
|
||||
// No response - give up this time
|
||||
return null;
|
||||
LOGGER.info(String.format("Error while trying to find common block with peer %s", peer));
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
LOGGER.trace(String.format("Received %s summar%s", blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y")));
|
||||
LOGGER.trace(String.format("Received %s summar%s", blockSummariesBatch.size(), (blockSummariesBatch.size() != 1 ? "ies" : "y")));
|
||||
|
||||
// Empty list means remote peer is unaware of test signature OR has no new blocks after test signature
|
||||
if (!blockSummaries.isEmpty())
|
||||
if (!blockSummariesBatch.isEmpty())
|
||||
// We have entries so we have found a common block
|
||||
break;
|
||||
|
||||
// No blocks after genesis block?
|
||||
// We don't get called for a peer at genesis height so this means NO blocks in common
|
||||
if (testHeight == 1)
|
||||
return Collections.emptyList();
|
||||
if (testHeight == 1) {
|
||||
LOGGER.info(String.format("Failure to find common block with peer %s", peer));
|
||||
return SynchronizationResult.NO_COMMON_BLOCK;
|
||||
}
|
||||
|
||||
// If common block is too far behind us then we're on massively different forks so give up.
|
||||
if (!force && testHeight < ourHeight - MAXIMUM_COMMON_DELTA) {
|
||||
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
|
||||
return SynchronizationResult.TOO_DIVERGENT;
|
||||
}
|
||||
|
||||
if (peer.getVersion() >= 2) {
|
||||
step <<= 1;
|
||||
@@ -373,20 +385,21 @@ public class Synchronizer {
|
||||
|
||||
// Prepend test block's summary as first block summary, as summaries returned are *after* test block
|
||||
BlockSummaryData testBlockSummary = new BlockSummaryData(testBlockData);
|
||||
blockSummaries.add(0, testBlockSummary);
|
||||
blockSummariesFromCommon.add(0, testBlockSummary);
|
||||
blockSummariesFromCommon.addAll(blockSummariesBatch);
|
||||
|
||||
// Trim summaries so that first summary is common block.
|
||||
// Currently we work back from the end until we hit a block we also have.
|
||||
// TODO: rewrite as modified binary search!
|
||||
for (int i = blockSummaries.size() - 1; i > 0; --i) {
|
||||
if (repository.getBlockRepository().exists(blockSummaries.get(i).getSignature())) {
|
||||
for (int i = blockSummariesFromCommon.size() - 1; i > 0; --i) {
|
||||
if (repository.getBlockRepository().exists(blockSummariesFromCommon.get(i).getSignature())) {
|
||||
// Note: index i isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
|
||||
blockSummaries.subList(0, i).clear();
|
||||
blockSummariesFromCommon.subList(0, i).clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return blockSummaries;
|
||||
return SynchronizationResult.OK;
|
||||
}
|
||||
|
||||
private List<BlockSummaryData> getBlockSummaries(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {
|
||||
|
@@ -2,10 +2,8 @@ package org.qortal.data.account;
|
||||
|
||||
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 org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -16,14 +14,17 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
|
||||
public class MintingAccountData {
|
||||
|
||||
// Properties
|
||||
|
||||
// Never actually displayed by API
|
||||
@Schema(hidden = true)
|
||||
@XmlTransient
|
||||
protected byte[] privateKey;
|
||||
|
||||
// Not always present - used by API if not null
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
// Read-only by API, we never ask for it as input
|
||||
@Schema(accessMode = AccessMode.READ_ONLY)
|
||||
protected byte[] publicKey;
|
||||
|
||||
// Not always present - used by API if not null
|
||||
protected String mintingAccount;
|
||||
protected String recipientAccount;
|
||||
protected String address;
|
||||
@@ -34,17 +35,17 @@ public class MintingAccountData {
|
||||
protected MintingAccountData() {
|
||||
}
|
||||
|
||||
public MintingAccountData(byte[] privateKey) {
|
||||
public MintingAccountData(byte[] privateKey, byte[] publicKey) {
|
||||
this.privateKey = privateKey;
|
||||
this.publicKey = PrivateKeyAccount.toPublicKey(privateKey);
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
public MintingAccountData(byte[] privateKey, RewardShareData rewardShareData) {
|
||||
this(privateKey);
|
||||
public MintingAccountData(MintingAccountData srcMintingAccountData, RewardShareData rewardShareData) {
|
||||
this(srcMintingAccountData.privateKey, srcMintingAccountData.publicKey);
|
||||
|
||||
if (rewardShareData != null) {
|
||||
this.recipientAccount = rewardShareData.getRecipient();
|
||||
this.mintingAccount = Crypto.toAddress(rewardShareData.getMinterPublicKey());
|
||||
this.mintingAccount = rewardShareData.getMinter();
|
||||
} else {
|
||||
this.address = Crypto.toAddress(this.publicKey);
|
||||
}
|
||||
@@ -56,8 +57,6 @@ public class MintingAccountData {
|
||||
return this.privateKey;
|
||||
}
|
||||
|
||||
@XmlElement(name = "publicKey")
|
||||
@Schema(accessMode = AccessMode.READ_ONLY)
|
||||
public byte[] getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
@@ -5,8 +5,9 @@ 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 org.qortal.crypto.Crypto;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@@ -14,6 +15,12 @@ public class RewardShareData {
|
||||
|
||||
// Properties
|
||||
private byte[] minterPublicKey;
|
||||
|
||||
// "minter" is called "mintingAccount" instead
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private String minter;
|
||||
|
||||
private String recipient;
|
||||
private byte[] rewardSharePublicKey;
|
||||
private BigDecimal sharePercent;
|
||||
@@ -25,8 +32,9 @@ public class RewardShareData {
|
||||
}
|
||||
|
||||
// Used when fetching from repository
|
||||
public RewardShareData(byte[] minterPublicKey, String recipient, byte[] rewardSharePublicKey, BigDecimal sharePercent) {
|
||||
public RewardShareData(byte[] minterPublicKey, String minter, String recipient, byte[] rewardSharePublicKey, BigDecimal sharePercent) {
|
||||
this.minterPublicKey = minterPublicKey;
|
||||
this.minter = minter;
|
||||
this.recipient = recipient;
|
||||
this.rewardSharePublicKey = rewardSharePublicKey;
|
||||
this.sharePercent = sharePercent;
|
||||
@@ -38,6 +46,10 @@ public class RewardShareData {
|
||||
return this.minterPublicKey;
|
||||
}
|
||||
|
||||
public String getMinter() {
|
||||
return this.minter;
|
||||
}
|
||||
|
||||
public String getRecipient() {
|
||||
return this.recipient;
|
||||
}
|
||||
@@ -52,7 +64,7 @@ public class RewardShareData {
|
||||
|
||||
@XmlElement(name = "mintingAccount")
|
||||
public String getMintingAccount() {
|
||||
return Crypto.toAddress(this.minterPublicKey);
|
||||
return this.minter;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -17,36 +17,32 @@ import org.apache.logging.log4j.Logger;
|
||||
public enum BIP39WordList {
|
||||
INSTANCE;
|
||||
|
||||
private Logger LOGGER = LogManager.getLogger(BIP39WordList.class);
|
||||
private static final Logger LOGGER = LogManager.getLogger(BIP39WordList.class);
|
||||
|
||||
private Map<String, List<String>> wordListsByLang;
|
||||
|
||||
private BIP39WordList() {
|
||||
wordListsByLang = new HashMap<>();
|
||||
}
|
||||
private static final Map<String, List<String>> wordListsByLang = new HashMap<>();
|
||||
|
||||
public synchronized List<String> getByLang(String lang) {
|
||||
List<String> wordList = wordListsByLang.get(lang);
|
||||
|
||||
if (wordList == null) {
|
||||
if (wordList == null && !wordListsByLang.containsKey(lang)) {
|
||||
ClassLoader loader = this.getClass().getClassLoader();
|
||||
|
||||
try (InputStream inputStream = loader.getResourceAsStream("BIP39/wordlist_" + lang + ".txt")) {
|
||||
if (inputStream == null) {
|
||||
if (inputStream == null)
|
||||
LOGGER.warn(String.format("Can't locate '%s' BIP39 wordlist", lang));
|
||||
return null;
|
||||
}
|
||||
|
||||
wordList = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.toList());
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn(String.format("Error reading '%s' BIP39 wordlist", lang), e);
|
||||
return null;
|
||||
LOGGER.warn(String.format("Error reading '%s' BIP39 wordlist: %s", lang, e.getMessage()));
|
||||
}
|
||||
|
||||
wordListsByLang.put(lang, wordList);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(wordList);
|
||||
if (wordList != null)
|
||||
return Collections.unmodifiableList(wordList);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import java.util.Map;
|
||||
import java.util.MissingFormatArgumentException;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -13,35 +14,12 @@ import org.apache.logging.log4j.Logger;
|
||||
public enum Translator {
|
||||
INSTANCE;
|
||||
|
||||
private final Logger LOGGER = LogManager.getLogger(Translator.class);
|
||||
private final String DEFAULT_LANG = Locale.getDefault().getLanguage();
|
||||
private static final Logger LOGGER = LogManager.getLogger(Translator.class);
|
||||
private static final String DEFAULT_LANG = Locale.getDefault().getLanguage();
|
||||
|
||||
private final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
|
||||
private static final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
|
||||
|
||||
private synchronized ResourceBundle getOrLoadResourceBundle(String className, String lang) {
|
||||
final String bundleKey = className + ":" + lang;
|
||||
|
||||
ResourceBundle resourceBundle = resourceBundles.get(bundleKey);
|
||||
if (resourceBundle != null)
|
||||
return resourceBundle;
|
||||
|
||||
try {
|
||||
resourceBundle = ResourceBundle.getBundle("i18n." + className, Locale.forLanguageTag(lang));
|
||||
} catch (MissingResourceException e) {
|
||||
LOGGER.warn("Can't locate '" + lang + "' translation resource bundle for " + className, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
resourceBundles.put(bundleKey, resourceBundle);
|
||||
|
||||
return resourceBundle;
|
||||
}
|
||||
|
||||
public String translate(final String className, final String key) {
|
||||
return this.translate(className, DEFAULT_LANG, key);
|
||||
}
|
||||
|
||||
public String translate(final String className, final String lang, final String key, final Object... args) {
|
||||
public String translate(String className, String lang, String key, Object... args) {
|
||||
ResourceBundle resourceBundle = getOrLoadResourceBundle(className, lang);
|
||||
|
||||
if (resourceBundle == null || !resourceBundle.containsKey(key))
|
||||
@@ -55,4 +33,37 @@ public enum Translator {
|
||||
}
|
||||
}
|
||||
|
||||
public String translate(String className, String key) {
|
||||
return this.translate(className, DEFAULT_LANG, key);
|
||||
}
|
||||
|
||||
public Set<String> keySet(String className, String lang) {
|
||||
ResourceBundle resourceBundle = getOrLoadResourceBundle(className, lang);
|
||||
|
||||
if (resourceBundle == null)
|
||||
return null;
|
||||
|
||||
return resourceBundle.keySet();
|
||||
}
|
||||
|
||||
private synchronized ResourceBundle getOrLoadResourceBundle(String className, String lang) {
|
||||
String bundleKey = className + ":" + lang;
|
||||
|
||||
ResourceBundle resourceBundle = resourceBundles.get(bundleKey);
|
||||
if (resourceBundle != null || resourceBundles.containsKey(bundleKey))
|
||||
return resourceBundle;
|
||||
|
||||
try {
|
||||
resourceBundle = ResourceBundle.getBundle("i18n." + className, Locale.forLanguageTag(lang));
|
||||
} catch (MissingResourceException e) {
|
||||
LOGGER.warn(String.format("Can't locate '%s' translation resource bundle for %s", lang, className));
|
||||
// Set to null then fall-through to storing in map so we don't emit warning more than once
|
||||
resourceBundle = null;
|
||||
}
|
||||
|
||||
resourceBundles.put(bundleKey, resourceBundle);
|
||||
|
||||
return resourceBundle;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -10,11 +10,14 @@ import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowFocusListener;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -30,7 +33,7 @@ import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.globalization.Translator;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.ui.UiService;
|
||||
import org.qortal.utils.RandomizeList;
|
||||
import org.qortal.utils.URLViewer;
|
||||
|
||||
public class SysTray {
|
||||
@@ -144,15 +147,11 @@ public class SysTray {
|
||||
}
|
||||
});
|
||||
|
||||
JMenuItem openUi = new JMenuItem(Translator.INSTANCE.translate("SysTray", "OPEN_NODE_UI"));
|
||||
JMenuItem openUi = new JMenuItem(Translator.INSTANCE.translate("SysTray", "OPEN_UI"));
|
||||
openUi.addActionListener(actionEvent -> {
|
||||
destroyHiddenDialog();
|
||||
|
||||
try {
|
||||
URLViewer.openWebpage(new URL("http://localhost:" + Settings.getInstance().getUiPort()));
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Unable to open node UI in browser");
|
||||
}
|
||||
new OpenUiWorker().execute();
|
||||
});
|
||||
menu.add(openUi);
|
||||
|
||||
@@ -174,7 +173,7 @@ public class SysTray {
|
||||
syncTime.addActionListener(actionEvent -> {
|
||||
destroyHiddenDialog();
|
||||
|
||||
new SynchronizeWorker().execute();
|
||||
new SynchronizeClockWorker().execute();
|
||||
});
|
||||
menu.add(syncTime);
|
||||
}
|
||||
@@ -190,11 +189,53 @@ public class SysTray {
|
||||
return menu;
|
||||
}
|
||||
|
||||
class SynchronizeWorker extends SwingWorker<Void, Void> {
|
||||
static class OpenUiWorker extends SwingWorker<Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
List<String> uiServers = new ArrayList<>();
|
||||
|
||||
String[] remoteUiServers = Settings.getInstance().getRemoteUiServers();
|
||||
uiServers.addAll(Arrays.asList(remoteUiServers));
|
||||
// Randomize remote servers
|
||||
uiServers = RandomizeList.randomize(uiServers);
|
||||
|
||||
// Prepend local servers
|
||||
String[] localUiServers = Settings.getInstance().getLocalUiServers();
|
||||
uiServers.addAll(0, Arrays.asList(localUiServers));
|
||||
|
||||
// Check each server in turn before opening browser tab
|
||||
int uiPort = Settings.getInstance().getUiServerPort();
|
||||
for (String uiServer : uiServers) {
|
||||
InetSocketAddress socketAddress = new InetSocketAddress(uiServer, uiPort);
|
||||
|
||||
// If we couldn't resolve try next
|
||||
if (socketAddress.isUnresolved())
|
||||
continue;
|
||||
|
||||
try (SocketChannel socketChannel = SocketChannel.open()) {
|
||||
socketChannel.socket().connect(socketAddress, 100);
|
||||
|
||||
// If we reach here, then socket connected to UI server!
|
||||
URLViewer.openWebpage(new URL(String.format("http://%s:%d", uiServer, uiPort)));
|
||||
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
// try next server
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Unable to open UI website in browser");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class SynchronizeClockWorker extends SwingWorker<Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
// Extract reconfiguration script from resources
|
||||
String resourceName = "/" + UiService.DOWNLOADS_RESOURCE_PATH + "/" + NTP_SCRIPT;
|
||||
String resourceName = "/node-management/" + NTP_SCRIPT;
|
||||
Path scriptPath = Paths.get(NTP_SCRIPT);
|
||||
|
||||
try (InputStream in = SysTray.class.getResourceAsStream(resourceName)) {
|
||||
@@ -218,7 +259,7 @@ public class SysTray {
|
||||
}
|
||||
}
|
||||
|
||||
class ClosingWorker extends SwingWorker<Void, Void> {
|
||||
static class ClosingWorker extends SwingWorker<Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
Controller.getInstance().shutdown();
|
||||
|
@@ -62,11 +62,10 @@ public enum Handshake {
|
||||
|
||||
// Is this ID already connected inbound or outbound?
|
||||
Peer otherInboundPeer = Network.getInstance().getInboundPeerWithId(peerId);
|
||||
Peer otherOutboundPeer = Network.getInstance().getOutboundHandshakedPeerWithId(peerId);
|
||||
|
||||
// Extra checks on inbound peers with known IDs, to prevent ID stealing
|
||||
if (!peer.isOutbound() && otherInboundPeer != null) {
|
||||
Peer otherOutboundPeer = Network.getInstance().getOutboundHandshakedPeerWithId(peerId);
|
||||
|
||||
if (otherOutboundPeer == null) {
|
||||
// We already have an inbound peer with this ID, but no outgoing peer with which to request verification
|
||||
LOGGER.trace(String.format("Discarding inbound peer %s with existing ID", peer));
|
||||
@@ -86,6 +85,11 @@ public enum Handshake {
|
||||
// Generate verification codes for later
|
||||
peer.generateVerificationCodes();
|
||||
}
|
||||
} else if (peer.isOutbound() && otherOutboundPeer != null) {
|
||||
// We already have an outbound connection to this peer?
|
||||
LOGGER.info(String.format("We already have another outbound connection to peer %s - discarding", peer));
|
||||
// Handshake failure - caller will deal with disconnect
|
||||
return null;
|
||||
} else {
|
||||
// Set peer's ID
|
||||
peer.setPeerId(peerId);
|
||||
@@ -231,7 +235,7 @@ public enum Handshake {
|
||||
private static void sendProof(Peer peer) {
|
||||
if (peer.isOutbound()) {
|
||||
// For outbound connections we need to generate real proof
|
||||
new Proof(peer).start();
|
||||
new Proof(peer).start(); // Calculate & send in a new thread to free up networking processing
|
||||
} else {
|
||||
// For incoming connections we only need to send a fake proof message as confirmation
|
||||
Message proofMessage = new ProofMessage(peer.getConnectionTimestamp(), 0, 0);
|
||||
|
@@ -56,6 +56,7 @@ import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.ExecuteProduceConsume;
|
||||
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
// For managing peers
|
||||
@@ -88,9 +89,14 @@ public class Network {
|
||||
"node4.qortal.org",
|
||||
"node5.qortal.org",
|
||||
"node6.qortal.org",
|
||||
"node7.qortal.org"
|
||||
"node7.qortal.org",
|
||||
"node8.qortal.org",
|
||||
"node9.qortal.org",
|
||||
"node10.qortal.org"
|
||||
};
|
||||
|
||||
private static final long NETWORK_EPC_KEEPALIVE = 10L; // seconds
|
||||
|
||||
public static final int MAX_SIGNATURES_PER_REPLY = 500;
|
||||
public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500;
|
||||
public static final int PEER_ID_LENGTH = 128;
|
||||
@@ -138,9 +144,10 @@ public class Network {
|
||||
|
||||
mergePeersLock = new ReentrantLock();
|
||||
|
||||
// We'll use a cached thread pool, max 10 threads, but with more aggressive 10 second timeout.
|
||||
ExecutorService networkExecutor = new ThreadPoolExecutor(1, 10,
|
||||
10L, TimeUnit.SECONDS,
|
||||
// We'll use a cached thread pool but with more aggressive timeout.
|
||||
ExecutorService networkExecutor = new ThreadPoolExecutor(1,
|
||||
Settings.getInstance().getMaxNetworkThreadPoolSize(),
|
||||
NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
|
||||
new SynchronousQueue<Runnable>());
|
||||
networkEPC = new NetworkProcessor(networkExecutor);
|
||||
}
|
||||
@@ -196,6 +203,10 @@ public class Network {
|
||||
return this.maxMessageSize;
|
||||
}
|
||||
|
||||
public StatsSnapshot getStatsSnapshot() {
|
||||
return this.networkEPC.getStatsSnapshot();
|
||||
}
|
||||
|
||||
// Peer lists
|
||||
|
||||
public List<Peer> getConnectedPeers() {
|
||||
@@ -294,15 +305,17 @@ public class Network {
|
||||
if (task != null)
|
||||
return task;
|
||||
|
||||
task = maybeProducePeerPingTask();
|
||||
final Long now = NTP.getTime();
|
||||
|
||||
task = maybeProducePeerPingTask(now);
|
||||
if (task != null)
|
||||
return task;
|
||||
|
||||
task = maybeProduceConnectPeerTask();
|
||||
task = maybeProduceConnectPeerTask(now);
|
||||
if (task != null)
|
||||
return task;
|
||||
|
||||
task = maybeProduceBroadcastTask();
|
||||
task = maybeProduceBroadcastTask(now);
|
||||
if (task != null)
|
||||
return task;
|
||||
|
||||
@@ -315,6 +328,65 @@ public class Network {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Task maybeProducePeerMessageTask() {
|
||||
for (Peer peer : getConnectedPeers()) {
|
||||
Task peerTask = peer.getMessageTask();
|
||||
if (peerTask != null)
|
||||
return peerTask;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Task maybeProducePeerPingTask(Long now) {
|
||||
// Ask connected peers whether they need a ping
|
||||
for (Peer peer : getConnectedPeers()) {
|
||||
Task peerTask = peer.getPingTask(now);
|
||||
if (peerTask != null)
|
||||
return peerTask;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class PeerConnectTask implements ExecuteProduceConsume.Task {
|
||||
private final Peer peer;
|
||||
|
||||
public PeerConnectTask(Peer peer) {
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform() throws InterruptedException {
|
||||
connectPeer(peer);
|
||||
}
|
||||
}
|
||||
|
||||
private Task maybeProduceConnectPeerTask(Long now) throws InterruptedException {
|
||||
if (now == null || now < nextConnectTaskTimestamp)
|
||||
return null;
|
||||
|
||||
if (getOutboundHandshakedPeers().size() >= minOutboundPeers)
|
||||
return null;
|
||||
|
||||
nextConnectTaskTimestamp = now + 1000L;
|
||||
|
||||
Peer targetPeer = getConnectablePeer(now);
|
||||
if (targetPeer == null)
|
||||
return null;
|
||||
|
||||
// Create connection task
|
||||
return new PeerConnectTask(targetPeer);
|
||||
}
|
||||
|
||||
private Task maybeProduceBroadcastTask(Long now) {
|
||||
if (now == null || now < nextBroadcastTimestamp)
|
||||
return null;
|
||||
|
||||
nextBroadcastTimestamp = now + BROADCAST_INTERVAL;
|
||||
return () -> Controller.getInstance().doNetworkBroadcast();
|
||||
}
|
||||
|
||||
class ChannelTask implements ExecuteProduceConsume.Task {
|
||||
private final SelectionKey selectionKey;
|
||||
|
||||
@@ -397,67 +469,6 @@ public class Network {
|
||||
|
||||
return new ChannelTask(nextSelectionKey);
|
||||
}
|
||||
|
||||
private Task maybeProducePeerMessageTask() {
|
||||
for (Peer peer : getConnectedPeers()) {
|
||||
Task peerTask = peer.getMessageTask();
|
||||
if (peerTask != null)
|
||||
return peerTask;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Task maybeProducePeerPingTask() {
|
||||
// Ask connected peers whether they need a ping
|
||||
for (Peer peer : getConnectedPeers()) {
|
||||
Task peerTask = peer.getPingTask();
|
||||
if (peerTask != null)
|
||||
return peerTask;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class PeerConnectTask implements ExecuteProduceConsume.Task {
|
||||
private final Peer peer;
|
||||
|
||||
public PeerConnectTask(Peer peer) {
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform() throws InterruptedException {
|
||||
connectPeer(peer);
|
||||
}
|
||||
}
|
||||
|
||||
private Task maybeProduceConnectPeerTask() throws InterruptedException {
|
||||
if (getOutboundHandshakedPeers().size() >= minOutboundPeers)
|
||||
return null;
|
||||
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null || now < nextConnectTaskTimestamp)
|
||||
return null;
|
||||
|
||||
nextConnectTaskTimestamp = now + 1000L;
|
||||
|
||||
Peer targetPeer = getConnectablePeer();
|
||||
if (targetPeer == null)
|
||||
return null;
|
||||
|
||||
// Create connection task
|
||||
return new PeerConnectTask(targetPeer);
|
||||
}
|
||||
|
||||
private Task maybeProduceBroadcastTask() {
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null || now < nextBroadcastTimestamp)
|
||||
return null;
|
||||
|
||||
nextBroadcastTimestamp = now + BROADCAST_INTERVAL;
|
||||
return () -> Controller.getInstance().doNetworkBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptConnection(ServerSocketChannel serverSocketChannel) throws InterruptedException {
|
||||
@@ -478,18 +489,20 @@ public class Network {
|
||||
|
||||
try {
|
||||
if (now == null) {
|
||||
LOGGER.debug(String.format("Connection discarded from peer %s due to lack of NTP sync", socketChannel.getRemoteAddress()));
|
||||
LOGGER.debug(() -> String.format("Connection discarded from peer %s due to lack of NTP sync", PeerAddress.fromSocket(socketChannel.socket())));
|
||||
socketChannel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this.connectedPeers) {
|
||||
if (connectedPeers.size() >= maxPeers) {
|
||||
// We have enough peers
|
||||
LOGGER.debug(String.format("Connection discarded from peer %s", socketChannel.getRemoteAddress()));
|
||||
LOGGER.debug(() -> String.format("Connection discarded from peer %s", PeerAddress.fromSocket(socketChannel.socket())));
|
||||
socketChannel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug(String.format("Connection accepted from peer %s", socketChannel.getRemoteAddress()));
|
||||
LOGGER.debug(() -> String.format("Connection accepted from peer %s", PeerAddress.fromSocket(socketChannel.socket())));
|
||||
|
||||
newPeer = new Peer(socketChannel);
|
||||
this.connectedPeers.add(newPeer);
|
||||
@@ -578,9 +591,27 @@ public class Network {
|
||||
}
|
||||
}
|
||||
|
||||
private Peer getConnectablePeer() throws InterruptedException {
|
||||
final long now = NTP.getTime();
|
||||
private final Predicate<PeerData> isSelfPeer = peerData -> {
|
||||
PeerAddress peerAddress = peerData.getAddress();
|
||||
return this.selfPeers.stream().anyMatch(selfPeer -> selfPeer.equals(peerAddress));
|
||||
};
|
||||
|
||||
private final Predicate<PeerData> isConnectedPeer = peerData -> {
|
||||
PeerAddress peerAddress = peerData.getAddress();
|
||||
return this.connectedPeers.stream().anyMatch(peer -> peer.getPeerData().getAddress().equals(peerAddress));
|
||||
};
|
||||
|
||||
private final Predicate<PeerData> isResolvedAsConnectedPeer = peerData -> {
|
||||
try {
|
||||
InetSocketAddress resolvedSocketAddress = peerData.getAddress().toSocketAddress();
|
||||
return this.connectedPeers.stream().anyMatch(peer -> peer.getResolvedAddress().equals(resolvedSocketAddress));
|
||||
} catch (UnknownHostException e) {
|
||||
// Can't resolve - no point even trying to connect
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private Peer getConnectablePeer(final Long now) throws InterruptedException {
|
||||
// We can't block here so use tryRepository(). We don't NEED to connect a new peer.
|
||||
try (final Repository repository = RepositoryManager.tryRepository()) {
|
||||
if (repository == null)
|
||||
@@ -596,36 +627,17 @@ public class Network {
|
||||
peerData.getLastAttempted() > lastAttemptedThreshold);
|
||||
|
||||
// Don't consider peers that we know loop back to ourself
|
||||
Predicate<PeerData> isSelfPeer = peerData -> {
|
||||
PeerAddress peerAddress = peerData.getAddress();
|
||||
return this.selfPeers.stream().anyMatch(selfPeer -> selfPeer.equals(peerAddress));
|
||||
};
|
||||
|
||||
synchronized (this.selfPeers) {
|
||||
peers.removeIf(isSelfPeer);
|
||||
}
|
||||
|
||||
// Don't consider already connected peers (simple address match)
|
||||
Predicate<PeerData> isConnectedPeer = peerData -> {
|
||||
PeerAddress peerAddress = peerData.getAddress();
|
||||
return this.connectedPeers.stream().anyMatch(peer -> peer.getPeerData().getAddress().equals(peerAddress));
|
||||
};
|
||||
|
||||
synchronized (this.connectedPeers) {
|
||||
peers.removeIf(isConnectedPeer);
|
||||
}
|
||||
|
||||
// Don't consider already connected peers (resolved address match)
|
||||
Predicate<PeerData> isResolvedAsConnectedPeer = peerData -> {
|
||||
try {
|
||||
InetSocketAddress resolvedSocketAddress = peerData.getAddress().toSocketAddress();
|
||||
return this.connectedPeers.stream().anyMatch(peer -> peer.getResolvedAddress().equals(resolvedSocketAddress));
|
||||
} catch (UnknownHostException e) {
|
||||
// Can't resolve - no point even trying to connect
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS
|
||||
synchronized (this.connectedPeers) {
|
||||
peers.removeIf(isResolvedAsConnectedPeer);
|
||||
}
|
||||
@@ -725,126 +737,43 @@ public class Network {
|
||||
|
||||
Handshake handshakeStatus = peer.getHandshakeStatus();
|
||||
if (handshakeStatus != Handshake.COMPLETED) {
|
||||
try {
|
||||
// Still handshaking
|
||||
LOGGER.trace(() -> String.format("Handshake status %s, message %s from peer %s", handshakeStatus.name(), (message != null ? message.getType().name() : "null"), peer));
|
||||
|
||||
// v1 nodes are keen on sending PINGs early. Send to back of queue so we'll process right after handshake
|
||||
if (message != null && message.getType() == MessageType.PING) {
|
||||
peer.queueMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check message type is as expected
|
||||
if (handshakeStatus.expectedMessageType != null && message.getType() != handshakeStatus.expectedMessageType) {
|
||||
LOGGER.debug(String.format("Unexpected %s message from %s, expected %s", message.getType().name(), peer, handshakeStatus.expectedMessageType));
|
||||
peer.disconnect("unexpected message");
|
||||
return;
|
||||
}
|
||||
|
||||
Handshake newHandshakeStatus = handshakeStatus.onMessage(peer, message);
|
||||
|
||||
if (newHandshakeStatus == null) {
|
||||
// Handshake failure
|
||||
LOGGER.debug(String.format("Handshake failure with peer %s message %s", peer, message.getType().name()));
|
||||
peer.disconnect("handshake failure");
|
||||
return;
|
||||
}
|
||||
|
||||
if (peer.isOutbound())
|
||||
// If we made outbound connection then we need to act first
|
||||
newHandshakeStatus.action(peer);
|
||||
else
|
||||
// We have inbound connection so we need to respond in kind with what we just received
|
||||
handshakeStatus.action(peer);
|
||||
|
||||
peer.setHandshakeStatus(newHandshakeStatus);
|
||||
|
||||
if (newHandshakeStatus == Handshake.COMPLETED)
|
||||
this.onHandshakeCompleted(peer);
|
||||
|
||||
return;
|
||||
} finally {
|
||||
peer.resetHandshakeMessagePending();
|
||||
}
|
||||
onHandshakingMessage(peer, message, handshakeStatus);
|
||||
return;
|
||||
}
|
||||
|
||||
// Should be non-handshaking messages from now on
|
||||
|
||||
// Ordered by message type value
|
||||
switch (message.getType()) {
|
||||
case PEER_VERIFY:
|
||||
// Remote peer wants extra verification
|
||||
possibleVerificationResponse(peer);
|
||||
case GET_PEERS:
|
||||
onGetPeersMessage(peer, message);
|
||||
break;
|
||||
|
||||
case VERIFICATION_CODES:
|
||||
VerificationCodesMessage verificationCodesMessage = (VerificationCodesMessage) message;
|
||||
case PEERS:
|
||||
onPeersMessage(peer, message);
|
||||
break;
|
||||
|
||||
// Remote peer is sending the code it wants to receive back via our outbound connection to it
|
||||
Peer ourUnverifiedPeer = Network.getInstance().getInboundPeerWithId(Network.getInstance().getOurPeerId());
|
||||
ourUnverifiedPeer.setVerificationCodes(verificationCodesMessage.getVerificationCodeSent(), verificationCodesMessage.getVerificationCodeExpected());
|
||||
|
||||
possibleVerificationResponse(ourUnverifiedPeer);
|
||||
case PING:
|
||||
onPingMessage(peer, message);
|
||||
break;
|
||||
|
||||
case VERSION:
|
||||
case PEER_ID:
|
||||
case PROOF:
|
||||
LOGGER.debug(String.format("Unexpected handshaking message %s from peer %s", message.getType().name(), peer));
|
||||
LOGGER.debug(() -> String.format("Unexpected handshaking message %s from peer %s", message.getType().name(), peer));
|
||||
peer.disconnect("unexpected handshaking message");
|
||||
return;
|
||||
|
||||
case PING:
|
||||
PingMessage pingMessage = (PingMessage) message;
|
||||
|
||||
// Generate 'pong' using same ID
|
||||
PingMessage pongMessage = new PingMessage();
|
||||
pongMessage.setId(pingMessage.getId());
|
||||
|
||||
if (!peer.sendMessage(pongMessage))
|
||||
peer.disconnect("failed to send ping reply");
|
||||
|
||||
break;
|
||||
|
||||
case PEERS:
|
||||
PeersMessage peersMessage = (PeersMessage) message;
|
||||
|
||||
List<PeerAddress> peerAddresses = new ArrayList<>();
|
||||
|
||||
// v1 PEERS message doesn't support port numbers so we have to add default port
|
||||
for (InetAddress peerAddress : peersMessage.getPeerAddresses())
|
||||
// This is always IPv4 so we don't have to worry about bracketing IPv6.
|
||||
peerAddresses.add(PeerAddress.fromString(peerAddress.getHostAddress()));
|
||||
|
||||
// Also add peer's details
|
||||
peerAddresses.add(PeerAddress.fromString(peer.getPeerData().getAddress().getHost()));
|
||||
|
||||
mergePeers(peer.toString(), peerAddresses);
|
||||
break;
|
||||
|
||||
case PEERS_V2:
|
||||
PeersV2Message peersV2Message = (PeersV2Message) message;
|
||||
|
||||
List<PeerAddress> peerV2Addresses = peersV2Message.getPeerAddresses();
|
||||
|
||||
// First entry contains remote peer's listen port but empty address.
|
||||
int peerPort = peerV2Addresses.get(0).getPort();
|
||||
peerV2Addresses.remove(0);
|
||||
|
||||
// If inbound peer, use listen port and socket address to recreate first entry
|
||||
if (!peer.isOutbound()) {
|
||||
PeerAddress sendingPeerAddress = PeerAddress.fromString(peer.getPeerData().getAddress().getHost() + ":" + peerPort);
|
||||
LOGGER.trace(() -> String.format("PEERS_V2 sending peer's listen address: %s", sendingPeerAddress.toString()));
|
||||
peerV2Addresses.add(0, sendingPeerAddress);
|
||||
}
|
||||
|
||||
mergePeers(peer.toString(), peerV2Addresses);
|
||||
onPeersV2Message(peer, message);
|
||||
break;
|
||||
|
||||
case GET_PEERS:
|
||||
// Send our known peers
|
||||
if (!peer.sendMessage(buildPeersMessage(peer)))
|
||||
peer.disconnect("failed to send peers list");
|
||||
case PEER_VERIFY:
|
||||
onPeerVerifyMessage(peer, message);
|
||||
break;
|
||||
|
||||
case VERIFICATION_CODES:
|
||||
onVerificationCodesMessage(peer, message);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -854,6 +783,116 @@ public class Network {
|
||||
}
|
||||
}
|
||||
|
||||
private void onHandshakingMessage(Peer peer, Message message, Handshake handshakeStatus) {
|
||||
try {
|
||||
// Still handshaking
|
||||
LOGGER.trace(() -> String.format("Handshake status %s, message %s from peer %s", handshakeStatus.name(), (message != null ? message.getType().name() : "null"), peer));
|
||||
|
||||
// v1 nodes are keen on sending PINGs early. Send to back of queue so we'll process right after handshake
|
||||
if (message != null && message.getType() == MessageType.PING) {
|
||||
peer.queueMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check message type is as expected
|
||||
if (handshakeStatus.expectedMessageType != null && message.getType() != handshakeStatus.expectedMessageType) {
|
||||
LOGGER.debug(() -> String.format("Unexpected %s message from %s, expected %s", message.getType().name(), peer, handshakeStatus.expectedMessageType));
|
||||
peer.disconnect("unexpected message");
|
||||
return;
|
||||
}
|
||||
|
||||
Handshake newHandshakeStatus = handshakeStatus.onMessage(peer, message);
|
||||
|
||||
if (newHandshakeStatus == null) {
|
||||
// Handshake failure
|
||||
LOGGER.debug(() -> String.format("Handshake failure with peer %s message %s", peer, message.getType().name()));
|
||||
peer.disconnect("handshake failure");
|
||||
return;
|
||||
}
|
||||
|
||||
if (peer.isOutbound())
|
||||
// If we made outbound connection then we need to act first
|
||||
newHandshakeStatus.action(peer);
|
||||
else
|
||||
// We have inbound connection so we need to respond in kind with what we just received
|
||||
handshakeStatus.action(peer);
|
||||
|
||||
peer.setHandshakeStatus(newHandshakeStatus);
|
||||
|
||||
if (newHandshakeStatus == Handshake.COMPLETED)
|
||||
this.onHandshakeCompleted(peer);
|
||||
} finally {
|
||||
peer.resetHandshakeMessagePending();
|
||||
}
|
||||
}
|
||||
|
||||
private void onGetPeersMessage(Peer peer, Message message) {
|
||||
// Send our known peers
|
||||
if (!peer.sendMessage(buildPeersMessage(peer)))
|
||||
peer.disconnect("failed to send peers list");
|
||||
}
|
||||
|
||||
private void onPeersMessage(Peer peer, Message message) {
|
||||
PeersMessage peersMessage = (PeersMessage) message;
|
||||
|
||||
List<PeerAddress> peerAddresses = new ArrayList<>();
|
||||
|
||||
// v1 PEERS message doesn't support port numbers so we have to add default port
|
||||
for (InetAddress peerAddress : peersMessage.getPeerAddresses())
|
||||
// This is always IPv4 so we don't have to worry about bracketing IPv6.
|
||||
peerAddresses.add(PeerAddress.fromString(peerAddress.getHostAddress()));
|
||||
|
||||
// Also add peer's details
|
||||
peerAddresses.add(PeerAddress.fromString(peer.getPeerData().getAddress().getHost()));
|
||||
|
||||
mergePeers(peer.toString(), peerAddresses);
|
||||
}
|
||||
|
||||
private void onPingMessage(Peer peer, Message message) {
|
||||
PingMessage pingMessage = (PingMessage) message;
|
||||
|
||||
// Generate 'pong' using same ID
|
||||
PingMessage pongMessage = new PingMessage();
|
||||
pongMessage.setId(pingMessage.getId());
|
||||
|
||||
if (!peer.sendMessage(pongMessage))
|
||||
peer.disconnect("failed to send ping reply");
|
||||
}
|
||||
|
||||
private void onPeersV2Message(Peer peer, Message message) {
|
||||
PeersV2Message peersV2Message = (PeersV2Message) message;
|
||||
|
||||
List<PeerAddress> peerV2Addresses = peersV2Message.getPeerAddresses();
|
||||
|
||||
// First entry contains remote peer's listen port but empty address.
|
||||
int peerPort = peerV2Addresses.get(0).getPort();
|
||||
peerV2Addresses.remove(0);
|
||||
|
||||
// If inbound peer, use listen port and socket address to recreate first entry
|
||||
if (!peer.isOutbound()) {
|
||||
PeerAddress sendingPeerAddress = PeerAddress.fromString(peer.getPeerData().getAddress().getHost() + ":" + peerPort);
|
||||
LOGGER.trace(() -> String.format("PEERS_V2 sending peer's listen address: %s", sendingPeerAddress.toString()));
|
||||
peerV2Addresses.add(0, sendingPeerAddress);
|
||||
}
|
||||
|
||||
mergePeers(peer.toString(), peerV2Addresses);
|
||||
}
|
||||
|
||||
private void onPeerVerifyMessage(Peer peer, Message message) {
|
||||
// Remote peer wants extra verification
|
||||
possibleVerificationResponse(peer);
|
||||
}
|
||||
|
||||
private void onVerificationCodesMessage(Peer peer, Message message) {
|
||||
VerificationCodesMessage verificationCodesMessage = (VerificationCodesMessage) message;
|
||||
|
||||
// Remote peer is sending the code it wants to receive back via our outbound connection to it
|
||||
Peer ourUnverifiedPeer = Network.getInstance().getInboundPeerWithId(Network.getInstance().getOurPeerId());
|
||||
ourUnverifiedPeer.setVerificationCodes(verificationCodesMessage.getVerificationCodeSent(), verificationCodesMessage.getVerificationCodeExpected());
|
||||
|
||||
possibleVerificationResponse(ourUnverifiedPeer);
|
||||
}
|
||||
|
||||
private void possibleVerificationResponse(Peer peer) {
|
||||
// Can't respond if we don't have the codes (yet?)
|
||||
if (peer.getVerificationCodeExpected() == null)
|
||||
|
@@ -43,7 +43,7 @@ public class Peer {
|
||||
private static final int CONNECT_TIMEOUT = 1000; // ms
|
||||
|
||||
/** Maximum time to wait for a message reply to arrive from peer. (ms) */
|
||||
private static final int RESPONSE_TIMEOUT = 5000; // ms
|
||||
private static final int RESPONSE_TIMEOUT = 2000; // ms
|
||||
|
||||
/**
|
||||
* Interval between PING messages to a peer. (ms)
|
||||
@@ -61,6 +61,8 @@ public class Peer {
|
||||
private InetSocketAddress resolvedAddress = null;
|
||||
/** True if remote address is loopback/link-local/site-local, false otherwise. */
|
||||
private boolean isLocal;
|
||||
|
||||
private final Object byteBufferLock = new Object();
|
||||
private volatile ByteBuffer byteBuffer;
|
||||
private Map<Integer, BlockingQueue<Message>> replyQueues;
|
||||
private LinkedBlockingQueue<Message> pendingMessages;
|
||||
@@ -256,7 +258,7 @@ public class Peer {
|
||||
this.connectionTimestamp = NTP.getTime();
|
||||
this.socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
|
||||
this.socketChannel.configureBlocking(false);
|
||||
this.byteBuffer = ByteBuffer.allocate(Network.getInstance().getMaxMessageSize());
|
||||
this.byteBuffer = null; // Defer allocation to when we need it, to save memory. Sorry GC!
|
||||
this.replyQueues = Collections.synchronizedMap(new HashMap<Integer, BlockingQueue<Message>>());
|
||||
this.pendingMessages = new LinkedBlockingQueue<>();
|
||||
}
|
||||
@@ -292,11 +294,15 @@ public class Peer {
|
||||
* @throws IOException
|
||||
*/
|
||||
/* package */ void readChannel() throws IOException {
|
||||
synchronized (this.byteBuffer) {
|
||||
synchronized (this.byteBufferLock) {
|
||||
while(true) {
|
||||
if (!this.socketChannel.isOpen() || this.socketChannel.socket().isClosed())
|
||||
return;
|
||||
|
||||
// Do we need to allocate byteBuffer?
|
||||
if (this.byteBuffer == null)
|
||||
this.byteBuffer = ByteBuffer.allocate(Network.getInstance().getMaxMessageSize());
|
||||
|
||||
final int bytesRead = this.socketChannel.read(this.byteBuffer);
|
||||
if (bytesRead == -1) {
|
||||
this.disconnect("EOF");
|
||||
@@ -318,9 +324,15 @@ public class Peer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message == null && bytesRead == 0 && !wasByteBufferFull)
|
||||
if (message == null && bytesRead == 0 && !wasByteBufferFull) {
|
||||
// No complete message in buffer, no more bytes to read from socket even though there was room to read bytes
|
||||
|
||||
// If byteBuffer is empty then we can deallocate it, to save memory, albeit costing GC
|
||||
if (this.byteBuffer.remaining() == this.byteBuffer.capacity())
|
||||
this.byteBuffer = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message == null)
|
||||
// No complete message in buffer, but maybe more bytes to read from socket
|
||||
@@ -452,18 +464,16 @@ public class Peer {
|
||||
}
|
||||
|
||||
/* package */ void startPings() {
|
||||
// Replacing initial null value allows pingCheck() to start sending pings.
|
||||
// Replacing initial null value allows getPingTask() to start sending pings.
|
||||
LOGGER.trace(() -> String.format("Enabling pings for peer %s", this));
|
||||
this.lastPingSent = System.currentTimeMillis();
|
||||
this.lastPingSent = NTP.getTime();
|
||||
}
|
||||
|
||||
/* package */ ExecuteProduceConsume.Task getPingTask() {
|
||||
/* package */ ExecuteProduceConsume.Task getPingTask(Long now) {
|
||||
// Pings not enabled yet?
|
||||
if (this.lastPingSent == null)
|
||||
if (now == null || this.lastPingSent == null)
|
||||
return null;
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
// Time to send another ping?
|
||||
if (now < this.lastPingSent + PING_INTERVAL)
|
||||
return null; // Not yet
|
||||
@@ -474,14 +484,14 @@ public class Peer {
|
||||
return () -> {
|
||||
PingMessage pingMessage = new PingMessage();
|
||||
Message message = this.getResponse(pingMessage);
|
||||
final long after = System.currentTimeMillis();
|
||||
|
||||
if (message == null || message.getType() != MessageType.PING) {
|
||||
LOGGER.debug(() -> String.format("Didn't receive reply from %s for PING ID %d", this, pingMessage.getId()));
|
||||
this.disconnect("no ping received");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setLastPing(after - now);
|
||||
this.setLastPing(NTP.getTime() - now);
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,8 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.HashSet;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.network.message.ProofMessage;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
@@ -13,6 +15,7 @@ public class Proof extends Thread {
|
||||
|
||||
private static final int MIN_PROOF_ZEROS = 2;
|
||||
private static final HashSet<Long> seenSalts = new HashSet<>();
|
||||
private static final Logger LOGGER = LogManager.getLogger(Proof.class);
|
||||
|
||||
private Peer peer;
|
||||
|
||||
@@ -38,6 +41,7 @@ public class Proof extends Thread {
|
||||
setName("Proof for peer " + this.peer);
|
||||
|
||||
// Do proof-of-work calculation to gain acceptance with remote end
|
||||
final long startTime = LOGGER.isTraceEnabled() ? System.currentTimeMillis() : 0;
|
||||
|
||||
// Remote end knows this (approximately)
|
||||
long timestamp = this.peer.getConnectionTimestamp();
|
||||
@@ -64,7 +68,7 @@ public class Proof extends Thread {
|
||||
long nonce;
|
||||
for (nonce = 0; nonce < Long.MAX_VALUE; ++nonce) {
|
||||
// Check whether we're shutting down every so often
|
||||
if ((nonce & 0xff) == 0 && (peer.isStopping() || Thread.currentThread().isInterrupted()))
|
||||
if ((nonce & 0xff) == 0 && (this.peer.isStopping() || Thread.currentThread().isInterrupted()))
|
||||
// throw new InterruptedException("Interrupted during peer proof calculation");
|
||||
return;
|
||||
|
||||
@@ -79,6 +83,8 @@ public class Proof extends Thread {
|
||||
sha256.reset();
|
||||
}
|
||||
|
||||
LOGGER.trace(() -> String.format("Proof for peer %s took %dms", this.peer, System.currentTimeMillis() - startTime));
|
||||
|
||||
ProofMessage proofMessage = new ProofMessage(timestamp, salt, nonce);
|
||||
peer.sendMessage(proofMessage);
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package org.qortal.repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
@@ -82,6 +83,12 @@ public interface AccountRepository {
|
||||
*/
|
||||
public void setMintedBlockCount(AccountData accountData) throws DataException;
|
||||
|
||||
/** Modifies account's minted block count only.
|
||||
* <p>
|
||||
* @return 2 if minted block count updated, 1 if block count set to delta, 0 if address not found.
|
||||
*/
|
||||
public int modifyMintedBlockCount(String address, int delta) throws DataException;
|
||||
|
||||
/** Delete account from repository. */
|
||||
public void delete(String address) throws DataException;
|
||||
|
||||
@@ -105,6 +112,8 @@ public interface AccountRepository {
|
||||
|
||||
public List<AccountBalanceData> getAssetBalances(List<String> addresses, List<Long> assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
public void modifyAssetBalance(String address, long assetId, BigDecimal deltaBalance) throws DataException;
|
||||
|
||||
public void save(AccountBalanceData accountBalanceData) throws DataException;
|
||||
|
||||
public void delete(String address, long assetId) throws DataException;
|
||||
@@ -155,6 +164,21 @@ public interface AccountRepository {
|
||||
|
||||
// Managing QORT from legacy QORA
|
||||
|
||||
/**
|
||||
* Returns balance data for accounts with legacy QORA asset that are eligible
|
||||
* for more block reward (block processing) or for block reward removal (block orphaning).
|
||||
* <p>
|
||||
* For block processing, accounts that have already received their final QORT reward for owning
|
||||
* legacy QORA are omitted from the results. <tt>blockHeight</tt> should be <tt>null</tt>.
|
||||
* <p>
|
||||
* For block orphaning, accounts that did not receive a QORT reward at <tt>blockHeight</tt>
|
||||
* are omitted from the results.
|
||||
*
|
||||
* @param blockHeight QORT reward must have be present at this height (for orphaning only)
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<AccountBalanceData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException;
|
||||
|
||||
public QortFromQoraData getQortFromQoraInfo(String address) throws DataException;
|
||||
|
||||
public void save(QortFromQoraData qortFromQoraData) throws DataException;
|
||||
|
@@ -4,9 +4,9 @@ import java.math.BigDecimal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
@@ -144,6 +144,10 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public void ensureAccount(AccountData accountData) throws DataException {
|
||||
/*
|
||||
* Why do we need to check/set the public_key?
|
||||
* Is there something that sets an account's balance which also needs to set the public key?
|
||||
|
||||
byte[] publicKey = accountData.getPublicKey();
|
||||
String sql = "SELECT public_key FROM Accounts WHERE account = ?";
|
||||
|
||||
@@ -168,6 +172,15 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to ensure minimal account in repository", e);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
String sql = "INSERT IGNORE INTO Accounts (account) VALUES (?)"; // MySQL syntax
|
||||
try {
|
||||
this.repository.checkedExecuteUpdateCount(sql, accountData.getAddress());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to ensure minimal account in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -273,6 +286,18 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int modifyMintedBlockCount(String address, int delta) throws DataException {
|
||||
String sql = "INSERT INTO Accounts (account, blocks_minted) VALUES (?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE blocks_minted = blocks_minted + ?";
|
||||
|
||||
try {
|
||||
return this.repository.checkedExecuteUpdateCount(sql, address, delta, delta);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to modify account's minted block count in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String address) throws DataException {
|
||||
// NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
|
||||
@@ -470,6 +495,54 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modifyAssetBalance(String address, long assetId, BigDecimal deltaBalance) throws DataException {
|
||||
// If deltaBalance is zero then do nothing
|
||||
if (deltaBalance.signum() == 0)
|
||||
return;
|
||||
|
||||
// If deltaBalance is negative then we assume AccountBalances & parent Accounts rows exist
|
||||
if (deltaBalance.signum() < 0) {
|
||||
// Perform actual balance change
|
||||
String sql = "UPDATE AccountBalances set balance = balance + ? WHERE account = ? AND asset_id = ?";
|
||||
try {
|
||||
this.repository.checkedExecuteUpdateCount(sql, deltaBalance, address, assetId);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to reduce account balance in repository", e);
|
||||
}
|
||||
|
||||
// If balance is now zero, and there are no prior historic balances, then simply delete row for this address-assetId (typically during orphaning)
|
||||
String deleteWhereSql = "account = ? AND asset_id = ? AND balance = 0 " + // covers "if balance now zero"
|
||||
"AND (" +
|
||||
"SELECT TRUE FROM HistoricAccountBalances " +
|
||||
"WHERE account = ? AND asset_id = ? AND height < (SELECT height - 1 FROM NextBlockHeight) " +
|
||||
"LIMIT 1" +
|
||||
")";
|
||||
try {
|
||||
this.repository.delete("AccountBalances", deleteWhereSql, address, assetId, address, assetId);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to prune account balance in repository", e);
|
||||
}
|
||||
} else {
|
||||
// We have to ensure parent row exists to satisfy foreign key constraint
|
||||
try {
|
||||
String sql = "INSERT IGNORE INTO Accounts (account) VALUES (?)"; // MySQL syntax
|
||||
this.repository.checkedExecuteUpdateCount(sql, address);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to ensure minimal account in repository", e);
|
||||
}
|
||||
|
||||
// Perform actual balance change
|
||||
String sql = "INSERT INTO AccountBalances (account, asset_id, balance) VALUES (?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE balance = balance + ?";
|
||||
try {
|
||||
this.repository.checkedExecuteUpdateCount(sql, address, assetId, deltaBalance, deltaBalance);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to increase account balance in repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(AccountBalanceData accountBalanceData) throws DataException {
|
||||
// If balance is zero and there are no prior historic balance, then simply delete balances for this assetId (typically during orphaning)
|
||||
@@ -490,13 +563,17 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
throw new DataException("Unable to delete account balance from repository", e);
|
||||
}
|
||||
|
||||
// I don't think we need to do this as Block.orphan() would do this for us?
|
||||
/*
|
||||
* I don't think we need to do this as Block.orphan() would do this for us?
|
||||
|
||||
try {
|
||||
this.repository.delete("HistoricAccountBalances", "account = ? AND asset_id = ?", accountBalanceData.getAddress(), accountBalanceData.getAssetId());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete historic account balances from repository", e);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -543,16 +620,17 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public RewardShareData getRewardShare(byte[] minterPublicKey, String recipient) throws DataException {
|
||||
String sql = "SELECT reward_share_public_key, share_percent FROM RewardShares WHERE minter_public_key = ? AND recipient = ?";
|
||||
String sql = "SELECT minter, reward_share_public_key, share_percent FROM RewardShares WHERE minter_public_key = ? AND recipient = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, minterPublicKey, recipient)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
byte[] rewardSharePublicKey = resultSet.getBytes(1);
|
||||
BigDecimal sharePercent = resultSet.getBigDecimal(2);
|
||||
String minter = resultSet.getString(1);
|
||||
byte[] rewardSharePublicKey = resultSet.getBytes(2);
|
||||
BigDecimal sharePercent = resultSet.getBigDecimal(3);
|
||||
|
||||
return new RewardShareData(minterPublicKey, recipient, rewardSharePublicKey, sharePercent);
|
||||
return new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch reward-share info from repository", e);
|
||||
}
|
||||
@@ -560,17 +638,18 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public RewardShareData getRewardShare(byte[] rewardSharePublicKey) throws DataException {
|
||||
String sql = "SELECT minter_public_key, recipient, share_percent FROM RewardShares WHERE reward_share_public_key = ?";
|
||||
String sql = "SELECT minter_public_key, minter, recipient, share_percent FROM RewardShares WHERE reward_share_public_key = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, rewardSharePublicKey)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
byte[] minterPublicKey = resultSet.getBytes(1);
|
||||
String recipient = resultSet.getString(2);
|
||||
BigDecimal sharePercent = resultSet.getBigDecimal(3);
|
||||
String minter = resultSet.getString(2);
|
||||
String recipient = resultSet.getString(3);
|
||||
BigDecimal sharePercent = resultSet.getBigDecimal(4);
|
||||
|
||||
return new RewardShareData(minterPublicKey, recipient, rewardSharePublicKey, sharePercent);
|
||||
return new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch reward-share info from repository", e);
|
||||
}
|
||||
@@ -598,7 +677,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public List<RewardShareData> getRewardShares() throws DataException {
|
||||
String sql = "SELECT minter_public_key, recipient, share_percent, reward_share_public_key FROM RewardShares";
|
||||
String sql = "SELECT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares";
|
||||
|
||||
List<RewardShareData> rewardShares = new ArrayList<>();
|
||||
|
||||
@@ -608,11 +687,12 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
do {
|
||||
byte[] minterPublicKey = resultSet.getBytes(1);
|
||||
String recipient = resultSet.getString(2);
|
||||
BigDecimal sharePercent = resultSet.getBigDecimal(3);
|
||||
byte[] rewardSharePublicKey = resultSet.getBytes(4);
|
||||
String minter = resultSet.getString(2);
|
||||
String recipient = resultSet.getString(3);
|
||||
BigDecimal sharePercent = resultSet.getBigDecimal(4);
|
||||
byte[] rewardSharePublicKey = resultSet.getBytes(5);
|
||||
|
||||
rewardShares.add(new RewardShareData(minterPublicKey, recipient, rewardSharePublicKey, sharePercent));
|
||||
rewardShares.add(new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent));
|
||||
} while (resultSet.next());
|
||||
|
||||
return rewardShares;
|
||||
@@ -625,7 +705,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
public List<RewardShareData> findRewardShares(List<String> minters, List<String> recipients, List<String> involvedAddresses,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT DISTINCT minter_public_key, recipient, share_percent, reward_share_public_key FROM RewardShares ");
|
||||
sql.append("SELECT DISTINCT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares ");
|
||||
|
||||
List<Object> args = new ArrayList<>();
|
||||
|
||||
@@ -695,11 +775,12 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
do {
|
||||
byte[] minterPublicKey = resultSet.getBytes(1);
|
||||
String recipient = resultSet.getString(2);
|
||||
BigDecimal sharePercent = resultSet.getBigDecimal(3);
|
||||
byte[] rewardSharePublicKey = resultSet.getBytes(4);
|
||||
String minter = resultSet.getString(2);
|
||||
String recipient = resultSet.getString(3);
|
||||
BigDecimal sharePercent = resultSet.getBigDecimal(4);
|
||||
byte[] rewardSharePublicKey = resultSet.getBytes(5);
|
||||
|
||||
rewardShares.add(new RewardShareData(minterPublicKey, recipient, rewardSharePublicKey, sharePercent));
|
||||
rewardShares.add(new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent));
|
||||
} while (resultSet.next());
|
||||
|
||||
return rewardShares;
|
||||
@@ -724,7 +805,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public RewardShareData getRewardShareByIndex(int index) throws DataException {
|
||||
String sql = "SELECT minter_public_key, recipient, share_percent, reward_share_public_key FROM RewardShares "
|
||||
String sql = "SELECT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares "
|
||||
+ "ORDER BY reward_share_public_key ASC "
|
||||
+ "OFFSET ? LIMIT 1";
|
||||
|
||||
@@ -733,11 +814,12 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
return null;
|
||||
|
||||
byte[] minterPublicKey = resultSet.getBytes(1);
|
||||
String recipient = resultSet.getString(2);
|
||||
BigDecimal sharePercent = resultSet.getBigDecimal(3);
|
||||
byte[] rewardSharePublicKey = resultSet.getBytes(4);
|
||||
String minter = resultSet.getString(2);
|
||||
String recipient = resultSet.getString(3);
|
||||
BigDecimal sharePercent = resultSet.getBigDecimal(4);
|
||||
byte[] rewardSharePublicKey = resultSet.getBytes(5);
|
||||
|
||||
return new RewardShareData(minterPublicKey, recipient, rewardSharePublicKey, sharePercent);
|
||||
return new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch reward-share info from repository", e);
|
||||
}
|
||||
@@ -747,8 +829,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
public void save(RewardShareData rewardShareData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("RewardShares");
|
||||
|
||||
saveHelper.bind("minter_public_key", rewardShareData.getMinterPublicKey()).bind("recipient", rewardShareData.getRecipient())
|
||||
.bind("reward_share_public_key", rewardShareData.getRewardSharePublicKey()).bind("share_percent", rewardShareData.getSharePercent());
|
||||
saveHelper.bind("minter_public_key", rewardShareData.getMinterPublicKey()).bind("minter", rewardShareData.getMinter())
|
||||
.bind("recipient", rewardShareData.getRecipient()).bind("reward_share_public_key", rewardShareData.getRewardSharePublicKey())
|
||||
.bind("share_percent", rewardShareData.getSharePercent());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
@@ -768,17 +851,19 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
// Minting accounts used by BlockMinter
|
||||
|
||||
@Override
|
||||
public List<MintingAccountData> getMintingAccounts() throws DataException {
|
||||
List<MintingAccountData> mintingAccounts = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute("SELECT minter_private_key FROM MintingAccounts")) {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute("SELECT minter_private_key, minter_public_key FROM MintingAccounts")) {
|
||||
if (resultSet == null)
|
||||
return mintingAccounts;
|
||||
|
||||
do {
|
||||
byte[] minterPrivateKey = resultSet.getBytes(1);
|
||||
byte[] minterPublicKey = resultSet.getBytes(2);
|
||||
|
||||
mintingAccounts.add(new MintingAccountData(minterPrivateKey));
|
||||
mintingAccounts.add(new MintingAccountData(minterPrivateKey, minterPublicKey));
|
||||
} while (resultSet.next());
|
||||
|
||||
return mintingAccounts;
|
||||
@@ -787,10 +872,12 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(MintingAccountData mintingAccountData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("MintingAccounts");
|
||||
|
||||
saveHelper.bind("minter_private_key", mintingAccountData.getPrivateKey());
|
||||
saveHelper.bind("minter_private_key", mintingAccountData.getPrivateKey())
|
||||
.bind("minter_public_key", mintingAccountData.getPublicKey());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
@@ -799,6 +886,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(byte[] minterPrivateKey) throws DataException {
|
||||
try {
|
||||
return this.repository.delete("MintingAccounts", "minter_private_key = ?", minterPrivateKey);
|
||||
@@ -809,6 +897,42 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
// Managing QORT from legacy QORA
|
||||
|
||||
@Override
|
||||
public List<AccountBalanceData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT account, balance from AccountBalances ");
|
||||
sql.append("LEFT OUTER JOIN AccountQortFromQoraInfo USING (account) ");
|
||||
sql.append("WHERE asset_id = ");
|
||||
sql.append(Asset.LEGACY_QORA); // int is safe to use literally
|
||||
sql.append(" AND (final_block_height IS NULL");
|
||||
|
||||
if (blockHeight != null) {
|
||||
sql.append(" OR final_block_height >= ");
|
||||
sql.append(blockHeight);
|
||||
}
|
||||
|
||||
sql.append(")");
|
||||
|
||||
List<AccountBalanceData> accountBalances = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
|
||||
if (resultSet == null)
|
||||
return accountBalances;
|
||||
|
||||
do {
|
||||
String address = resultSet.getString(1);
|
||||
BigDecimal balance = resultSet.getBigDecimal(2).setScale(8);
|
||||
|
||||
accountBalances.add(new AccountBalanceData(address, Asset.LEGACY_QORA, balance));
|
||||
} while (resultSet.next());
|
||||
|
||||
return accountBalances;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch eligible legacy QORA holders from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public QortFromQoraData getQortFromQoraInfo(String address) throws DataException {
|
||||
String sql = "SELECT final_qort_from_qora, final_block_height FROM AccountQortFromQoraInfo WHERE account = ?";
|
||||
|
||||
@@ -827,6 +951,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(QortFromQoraData qortFromQoraData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountQortFromQoraInfo");
|
||||
|
||||
@@ -841,6 +966,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteQortFromQoraInfo(String address) throws DataException {
|
||||
try {
|
||||
return this.repository.delete("AccountQortFromQoraInfo", "account = ?", address);
|
||||
|
@@ -201,7 +201,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
String subquerySql = "SELECT minter, COUNT(signature) FROM Blocks GROUP BY minter";
|
||||
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT DISTINCT block_minter, n_blocks, minter_public_key, recipient FROM (");
|
||||
sql.append("SELECT DISTINCT block_minter, n_blocks, minter_public_key, minter, recipient FROM (");
|
||||
sql.append(subquerySql);
|
||||
sql.append(") AS Minters (block_minter, n_blocks) LEFT OUTER JOIN RewardShares ON reward_share_public_key = block_minter ");
|
||||
|
||||
@@ -239,14 +239,17 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
do {
|
||||
byte[] blockMinterPublicKey = resultSet.getBytes(1);
|
||||
int nBlocks = resultSet.getInt(2);
|
||||
|
||||
// May not be present if no reward-share:
|
||||
byte[] mintingAccountPublicKey = resultSet.getBytes(3);
|
||||
String recipientAccount = resultSet.getString(4);
|
||||
String minterAccount = resultSet.getString(4);
|
||||
String recipientAccount = resultSet.getString(5);
|
||||
|
||||
BlockMinterSummary blockMinterSummary;
|
||||
if (recipientAccount == null)
|
||||
blockMinterSummary = new BlockMinterSummary(blockMinterPublicKey, nBlocks);
|
||||
else
|
||||
blockMinterSummary = new BlockMinterSummary(blockMinterPublicKey, nBlocks, mintingAccountPublicKey, recipientAccount);
|
||||
blockMinterSummary = new BlockMinterSummary(blockMinterPublicKey, nBlocks, mintingAccountPublicKey, minterAccount, recipientAccount);
|
||||
|
||||
summaries.add(blockMinterSummary);
|
||||
} while (resultSet.next());
|
||||
@@ -260,13 +263,13 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
@Override
|
||||
public List<BlockSummaryData> getBlockSummariesByMinter(byte[] minterPublicKey, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
sql.append("SELECT signature, height, minter, online_accounts_count FROM ");
|
||||
sql.append("SELECT signature, height, Blocks.minter, online_accounts_count FROM ");
|
||||
|
||||
// List of minter account's public key and reward-share public keys with minter's public key
|
||||
sql.append("(SELECT * FROM (VALUES (CAST(? AS QortalPublicKey))) UNION (SELECT reward_share_public_key FROM RewardShares WHERE minter_public_key = ?)) AS PublicKeys (public_key) ");
|
||||
|
||||
// Match Blocks signed with public key from above list
|
||||
sql.append("JOIN Blocks ON minter = public_key ");
|
||||
sql.append("JOIN Blocks ON Blocks.minter = public_key ");
|
||||
|
||||
sql.append("ORDER BY Blocks.height ");
|
||||
if (reverse != null && reverse)
|
||||
|
@@ -928,6 +928,34 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS HistoricAccountBalancesHeightIndex ON HistoricAccountBalances (height)");
|
||||
break;
|
||||
|
||||
case 66:
|
||||
// Add CHECK constraint to account balances
|
||||
stmt.execute("ALTER TABLE AccountBalances ADD CONSTRAINT CheckBalanceNotNegative CHECK (balance >= 0)");
|
||||
break;
|
||||
|
||||
case 67:
|
||||
// Provide external function to convert private keys to public keys
|
||||
stmt.execute("CREATE FUNCTION Ed25519_private_to_public_key (IN privateKey VARBINARY(32)) RETURNS VARBINARY(32) LANGUAGE JAVA DETERMINISTIC NO SQL EXTERNAL NAME 'CLASSPATH:org.qortal.repository.hsqldb.HSQLDBRepository.ed25519PrivateToPublicKey'");
|
||||
|
||||
// Cache minting account public keys to save us recalculating them
|
||||
stmt.execute("ALTER TABLE MintingAccounts ADD minter_public_key QortalPublicKey");
|
||||
stmt.execute("UPDATE MintingAccounts SET minter_public_key = Ed25519_private_to_public_key(minter_private_key)");
|
||||
stmt.execute("ALTER TABLE MintingAccounts ALTER COLUMN minter_public_key SET NOT NULL");
|
||||
|
||||
// Provide external function to convert public keys to addresses
|
||||
stmt.execute("CREATE FUNCTION Ed25519_public_key_to_address (IN privateKey VARBINARY(32)) RETURNS VARCHAR(36) LANGUAGE JAVA DETERMINISTIC NO SQL EXTERNAL NAME 'CLASSPATH:org.qortal.repository.hsqldb.HSQLDBRepository.ed25519PublicKeyToAddress'");
|
||||
|
||||
// Cache reward-share minting account's address
|
||||
stmt.execute("ALTER TABLE RewardShares ADD minter QortalAddress BEFORE recipient");
|
||||
stmt.execute("UPDATE RewardShares SET minter = Ed25519_public_key_to_address(minter_public_key)");
|
||||
stmt.execute("ALTER TABLE RewardShares ALTER COLUMN minter SET NOT NULL");
|
||||
break;
|
||||
|
||||
case 68:
|
||||
// Slow down log fsync() calls from every 500ms to reduce I/O load
|
||||
stmt.execute("SET FILES WRITE DELAY 5"); // only fsync() every 5 seconds
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@@ -28,6 +28,8 @@ import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.ATRepository;
|
||||
import org.qortal.repository.AccountRepository;
|
||||
import org.qortal.repository.ArbitraryRepository;
|
||||
@@ -57,6 +59,8 @@ public class HSQLDBRepository implements Repository {
|
||||
protected List<String> sqlStatements;
|
||||
protected long sessionId;
|
||||
|
||||
// Constructors
|
||||
|
||||
// NB: no visibility modifier so only callable from within same package
|
||||
/* package */ HSQLDBRepository(Connection connection) throws DataException {
|
||||
this.connection = connection;
|
||||
@@ -84,6 +88,8 @@ public class HSQLDBRepository implements Repository {
|
||||
assertEmptyTransaction("connection creation");
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
|
||||
@Override
|
||||
public ATRepository getATRepository() {
|
||||
return new HSQLDBATRepository(this);
|
||||
@@ -134,6 +140,18 @@ public class HSQLDBRepository implements Repository {
|
||||
return new HSQLDBVotingRepository(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getDebug() {
|
||||
return this.debugState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDebug(boolean debugState) {
|
||||
this.debugState = debugState;
|
||||
}
|
||||
|
||||
// Transaction COMMIT / ROLLBACK / savepoints
|
||||
|
||||
@Override
|
||||
public void saveChanges() throws DataException {
|
||||
try {
|
||||
@@ -203,6 +221,8 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
}
|
||||
|
||||
// Close / backup / rebuild / restore
|
||||
|
||||
@Override
|
||||
public void close() throws DataException {
|
||||
// Already closed? No need to do anything but maybe report double-call
|
||||
@@ -257,16 +277,6 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getDebug() {
|
||||
return this.debugState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDebug(boolean debugState) {
|
||||
this.debugState = debugState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backup(boolean quick) throws DataException {
|
||||
if (!quick)
|
||||
@@ -337,8 +347,8 @@ public class HSQLDBRepository implements Repository {
|
||||
Path oldRepoFilePath = oldRepoPath.getFileName();
|
||||
|
||||
// Try to open backup. We need to remove "create=true" and insert "backup" dir before final filename.
|
||||
String backupUrlTemplate = "jdbc:hsqldb:file:%s/backup/%s;create=false;hsqldb.full_log_replay=true";
|
||||
return String.format(backupUrlTemplate, oldRepoDirPath.toString(), oldRepoFilePath.toString());
|
||||
String backupUrlTemplate = "jdbc:hsqldb:file:%s%sbackup%s%s;create=false;hsqldb.full_log_replay=true";
|
||||
return String.format(backupUrlTemplate, oldRepoDirPath.toString(), File.separator, File.separator, oldRepoFilePath.toString());
|
||||
}
|
||||
|
||||
/* package */ static void attemptRecovery(String connectionUrl) throws DataException {
|
||||
@@ -361,8 +371,8 @@ public class HSQLDBRepository implements Repository {
|
||||
.forEach(File::delete);
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
// Now "backup" the backup back to original repository location (the parent)
|
||||
// NOTE: trailing / is OK because HSQLDB checks for both / and O/S-specific separator
|
||||
// Now "backup" the backup back to original repository location (the parent).
|
||||
// NOTE: trailing / is OK because HSQLDB checks for both / and O/S-specific separator.
|
||||
// textdb.allow_full_path connection property is required to be able to use '..'
|
||||
stmt.execute("BACKUP DATABASE TO '../' BLOCKING AS FILES");
|
||||
} catch (SQLException e) {
|
||||
@@ -386,6 +396,8 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
}
|
||||
|
||||
// SQL statements, etc.
|
||||
|
||||
/**
|
||||
* Returns prepared statement using passed SQL, logging query if necessary.
|
||||
*/
|
||||
@@ -399,19 +411,6 @@ public class HSQLDBRepository implements Repository {
|
||||
return this.connection.prepareStatement(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs this transaction's SQL statements, if enabled.
|
||||
*/
|
||||
public void logStatements() {
|
||||
if (this.sqlStatements == null)
|
||||
return;
|
||||
|
||||
LOGGER.info(String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId));
|
||||
|
||||
for (String sql : this.sqlStatements)
|
||||
LOGGER.info(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute SQL and return ResultSet with but added checking.
|
||||
* <p>
|
||||
@@ -429,15 +428,18 @@ public class HSQLDBRepository implements Repository {
|
||||
// We can't use try-with-resources here as closing the PreparedStatement on return would also prematurely close the ResultSet.
|
||||
preparedStatement.closeOnCompletion();
|
||||
|
||||
long beforeQuery = System.currentTimeMillis();
|
||||
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||
|
||||
ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement, objects);
|
||||
|
||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||
if (this.slowQueryThreshold != null && queryTime > this.slowQueryThreshold) {
|
||||
LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
|
||||
if (this.slowQueryThreshold != null) {
|
||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||
|
||||
logStatements();
|
||||
if (queryTime > this.slowQueryThreshold) {
|
||||
LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
|
||||
|
||||
logStatements();
|
||||
}
|
||||
}
|
||||
|
||||
return resultSet;
|
||||
@@ -500,16 +502,19 @@ public class HSQLDBRepository implements Repository {
|
||||
try (PreparedStatement preparedStatement = this.prepareStatement(sql)) {
|
||||
prepareExecute(preparedStatement, objects);
|
||||
|
||||
long beforeQuery = System.currentTimeMillis();
|
||||
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||
|
||||
if (preparedStatement.execute())
|
||||
throw new SQLException("Database produced results, not row count");
|
||||
|
||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||
if (this.slowQueryThreshold != null && queryTime > this.slowQueryThreshold) {
|
||||
LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
|
||||
if (this.slowQueryThreshold != null) {
|
||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||
|
||||
logStatements();
|
||||
if (queryTime > this.slowQueryThreshold) {
|
||||
LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
|
||||
|
||||
logStatements();
|
||||
}
|
||||
}
|
||||
|
||||
int rowCount = preparedStatement.getUpdateCount();
|
||||
@@ -670,6 +675,21 @@ public class HSQLDBRepository implements Repository {
|
||||
stringBuilder.append(") ");
|
||||
}
|
||||
|
||||
// Debugging
|
||||
|
||||
/**
|
||||
* Logs this transaction's SQL statements, if enabled.
|
||||
*/
|
||||
public void logStatements() {
|
||||
if (this.sqlStatements == null)
|
||||
return;
|
||||
|
||||
LOGGER.info(String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId));
|
||||
|
||||
for (String sql : this.sqlStatements)
|
||||
LOGGER.info(sql);
|
||||
}
|
||||
|
||||
/** Logs other HSQLDB sessions then re-throws passed exception */
|
||||
public SQLException examineException(SQLException e) throws SQLException {
|
||||
LOGGER.error(String.format("HSQLDB error (session %d): %s", this.sessionId, e.getMessage()), e);
|
||||
@@ -726,6 +746,22 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
|
||||
public static byte[] ed25519PrivateToPublicKey(byte[] privateKey) {
|
||||
if (privateKey == null)
|
||||
return null;
|
||||
|
||||
return PrivateKeyAccount.toPublicKey(privateKey);
|
||||
}
|
||||
|
||||
public static String ed25519PublicKeyToAddress(byte[] publicKey) {
|
||||
if (publicKey == null)
|
||||
return null;
|
||||
|
||||
return Crypto.toAddress(publicKey);
|
||||
}
|
||||
|
||||
/** Converts milliseconds from epoch to OffsetDateTime needed for TIMESTAMP WITH TIME ZONE columns. */
|
||||
/* package */ static OffsetDateTime toOffsetDateTime(Long timestamp) {
|
||||
if (timestamp == null)
|
||||
|
@@ -31,9 +31,6 @@ public class Settings {
|
||||
private static final int MAINNET_API_PORT = 12391;
|
||||
private static final int TESTNET_API_PORT = 62391;
|
||||
|
||||
private static final int MAINNET_UI_PORT = 12390;
|
||||
private static final int TESTNET_UI_PORT = 62390;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(Settings.class);
|
||||
private static final String SETTINGS_FILENAME = "settings.json";
|
||||
|
||||
@@ -43,14 +40,17 @@ public class Settings {
|
||||
// Settings, and other config files
|
||||
private String userPath;
|
||||
|
||||
// Common to all networking (UI/API/P2P)
|
||||
// Common to all networking (API/P2P)
|
||||
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
|
||||
|
||||
// Node management UI
|
||||
private boolean uiEnabled = true;
|
||||
private Integer uiPort;
|
||||
private String[] uiWhitelist = new String[] {
|
||||
"::1", "127.0.0.1"
|
||||
// UI servers
|
||||
private int uiPort = 12388;
|
||||
private String[] uiLocalServers = new String[] {
|
||||
"localhost", "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",
|
||||
"node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org"
|
||||
};
|
||||
|
||||
// API-related
|
||||
@@ -71,17 +71,21 @@ public class Settings {
|
||||
private int maxTransactionTimestampFuture = 24 * 60 * 60 * 1000; // milliseconds
|
||||
/** Whether we check, fetch and install auto-updates */
|
||||
private boolean autoUpdateEnabled = true;
|
||||
/** Whether to show a notification when we backup repository. */
|
||||
private boolean showBackupNotification = false;
|
||||
|
||||
// Peer-to-peer related
|
||||
private boolean isTestNet = false;
|
||||
/** Port number for inbound peer-to-peer connections. */
|
||||
private Integer listenPort;
|
||||
/** Minimum number of peers to allow block minting / synchronization. */
|
||||
private int minBlockchainPeers = 5;
|
||||
private int minBlockchainPeers = 10;
|
||||
/** Target number of outbound connections to peers we should make. */
|
||||
private int minOutboundPeers = 20;
|
||||
private int minOutboundPeers = 25;
|
||||
/** Maximum number of peer connections we allow. */
|
||||
private int maxPeers = 50;
|
||||
/** Maximum number of threads for network engine. */
|
||||
private int maxNetworkThreadPoolSize = 20;
|
||||
|
||||
// Which blockchains this node is running
|
||||
private String blockchainConfig = null; // use default from resources
|
||||
@@ -113,7 +117,7 @@ public class Settings {
|
||||
"3.cn.pool.ntp.org"
|
||||
};
|
||||
/** Additional offset added to values returned by NTP.getTime() */
|
||||
private long testNtpOffset = 0;
|
||||
private Long testNtpOffset = null;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -244,19 +248,16 @@ public class Settings {
|
||||
return this.userPath;
|
||||
}
|
||||
|
||||
public boolean isUiEnabled() {
|
||||
return this.uiEnabled;
|
||||
public int getUiServerPort() {
|
||||
return this.uiPort;
|
||||
}
|
||||
|
||||
public int getUiPort() {
|
||||
if (this.uiPort != null)
|
||||
return this.uiPort;
|
||||
|
||||
return this.isTestNet ? TESTNET_UI_PORT : MAINNET_UI_PORT;
|
||||
public String[] getLocalUiServers() {
|
||||
return this.uiLocalServers;
|
||||
}
|
||||
|
||||
public String[] getUiWhitelist() {
|
||||
return this.uiWhitelist;
|
||||
public String[] getRemoteUiServers() {
|
||||
return this.uiRemoteServers;
|
||||
}
|
||||
|
||||
public boolean isApiEnabled() {
|
||||
@@ -334,6 +335,10 @@ public class Settings {
|
||||
return this.maxPeers;
|
||||
}
|
||||
|
||||
public int getMaxNetworkThreadPoolSize() {
|
||||
return this.maxNetworkThreadPoolSize;
|
||||
}
|
||||
|
||||
public String getBlockchainConfig() {
|
||||
return this.blockchainConfig;
|
||||
}
|
||||
@@ -362,8 +367,12 @@ public class Settings {
|
||||
return this.ntpServers;
|
||||
}
|
||||
|
||||
public long getTestNtpOffset() {
|
||||
public Long getTestNtpOffset() {
|
||||
return this.testNtpOffset;
|
||||
}
|
||||
|
||||
public boolean getShowBackupNotification() {
|
||||
return this.showBackupNotification;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -195,7 +195,9 @@ public class RewardShareTransaction extends Transaction {
|
||||
this.repository.getAccountRepository().delete(mintingAccount.getPublicKey(), rewardShareTransactionData.getRecipient());
|
||||
} else {
|
||||
// Save reward-share info
|
||||
rewardShareData = new RewardShareData(mintingAccount.getPublicKey(), rewardShareTransactionData.getRecipient(), rewardShareTransactionData.getRewardSharePublicKey(), rewardShareTransactionData.getSharePercent());
|
||||
rewardShareData = new RewardShareData(mintingAccount.getPublicKey(), mintingAccount.getAddress(),
|
||||
rewardShareTransactionData.getRecipient(), rewardShareTransactionData.getRewardSharePublicKey(),
|
||||
rewardShareTransactionData.getSharePercent());
|
||||
this.repository.getAccountRepository().save(rewardShareData);
|
||||
}
|
||||
}
|
||||
@@ -217,8 +219,9 @@ public class RewardShareTransaction extends Transaction {
|
||||
|
||||
if (rewardShareTransactionData.getPreviousSharePercent() != null) {
|
||||
// Revert previous sharing arrangement
|
||||
RewardShareData rewardShareData = new RewardShareData(mintingAccount.getPublicKey(), rewardShareTransactionData.getRecipient(),
|
||||
rewardShareTransactionData.getRewardSharePublicKey(), rewardShareTransactionData.getPreviousSharePercent());
|
||||
RewardShareData rewardShareData = new RewardShareData(mintingAccount.getPublicKey(), mintingAccount.getAddress(),
|
||||
rewardShareTransactionData.getRecipient(), rewardShareTransactionData.getRewardSharePublicKey(),
|
||||
rewardShareTransactionData.getPreviousSharePercent());
|
||||
|
||||
this.repository.getAccountRepository().save(rewardShareData);
|
||||
} else {
|
||||
|
@@ -241,6 +241,7 @@ public abstract class Transaction {
|
||||
ASSET_NOT_SPENDABLE(89),
|
||||
ACCOUNT_CANNOT_REWARD_SHARE(90),
|
||||
SELF_SHARE_EXISTS(91),
|
||||
ACCOUNT_ALREADY_EXISTS(92),
|
||||
NOT_YET_RELEASED(1000);
|
||||
|
||||
public final int value;
|
||||
|
@@ -83,6 +83,10 @@ public class TransferPrivsTransaction extends Transaction {
|
||||
if (!Crypto.isValidAddress(this.transferPrivsTransactionData.getRecipient()))
|
||||
return ValidationResult.INVALID_ADDRESS;
|
||||
|
||||
// Check recipient is new account
|
||||
if (this.repository.getAccountRepository().accountExists(this.transferPrivsTransactionData.getRecipient()))
|
||||
return ValidationResult.ACCOUNT_ALREADY_EXISTS;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@@ -183,8 +187,11 @@ public class TransferPrivsTransaction extends Transaction {
|
||||
accountRepository.setFlags(senderData);
|
||||
|
||||
// Restore recipient's flags
|
||||
recipientData.setFlags(this.transferPrivsTransactionData.getPreviousRecipientFlags());
|
||||
accountRepository.setFlags(recipientData);
|
||||
Integer previousRecipientFlags = this.transferPrivsTransactionData.getPreviousRecipientFlags();
|
||||
if (previousRecipientFlags != null) {
|
||||
recipientData.setFlags(previousRecipientFlags);
|
||||
accountRepository.setFlags(recipientData);
|
||||
}
|
||||
|
||||
// Clean values in transaction data
|
||||
this.transferPrivsTransactionData.setPreviousSenderFlags(null);
|
||||
|
@@ -1,46 +0,0 @@
|
||||
package org.qortal.ui;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.http.HttpContent;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.server.ResourceService;
|
||||
import org.eclipse.jetty.util.URIUtil;
|
||||
|
||||
/**
|
||||
* Replace ResourceService that delivers content as "attachments", typically forcing download instead of rendering.
|
||||
* <p>
|
||||
* Sets <tt>Content-Type</tt> header to <tt>application/octet-stream</tt><br>
|
||||
* Sets <tt>Content-Disposition</tt> header to <tt>attachment; filename="<i>basename</i>"</tt><br>
|
||||
* where <i>basename</i> is that last component of requested URI path.
|
||||
* <p>
|
||||
* Example usage:<br>
|
||||
* <br>
|
||||
* <tt>... = new ServletHolder("servlet-name", new DefaultServlet(new DownloadResourceService()));</tt>
|
||||
*/
|
||||
public class DownloadResourceService extends ResourceService {
|
||||
|
||||
@Override
|
||||
protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, final HttpContent content, Enumeration<String> reqRanges) throws IOException {
|
||||
final boolean _pathInfoOnly = super.isPathInfoOnly();
|
||||
String servletPath = _pathInfoOnly ? "/" : request.getServletPath();
|
||||
String pathInfo = request.getPathInfo();
|
||||
String pathInContext = URIUtil.addPaths(servletPath,pathInfo);
|
||||
|
||||
// Find basename of requested content
|
||||
final int slashIndex = pathInContext.lastIndexOf(URIUtil.SLASH);
|
||||
if (slashIndex != -1)
|
||||
pathInContext = pathInContext.substring(slashIndex + 1);
|
||||
|
||||
// Add appropriate headers
|
||||
response.setHeader(HttpHeader.CONTENT_TYPE.asString(), "application/octet-stream");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + pathInContext + "\"");
|
||||
|
||||
return super.sendData(request, response, include, content, reqRanges);
|
||||
}
|
||||
|
||||
}
|
@@ -1,101 +0,0 @@
|
||||
package org.qortal.ui;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.InetAccessHandler;
|
||||
import org.eclipse.jetty.servlet.DefaultServlet;
|
||||
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.qortal.settings.Settings;
|
||||
|
||||
public class UiService {
|
||||
|
||||
public static final String DOWNLOADS_RESOURCE_PATH = "node-ui-downloads";
|
||||
private static UiService instance;
|
||||
|
||||
private Server server;
|
||||
|
||||
private UiService() {
|
||||
}
|
||||
|
||||
public static UiService getInstance() {
|
||||
if (instance == null)
|
||||
instance = new UiService();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
try {
|
||||
// Create node management UI server
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getUiPort());
|
||||
this.server = new Server(endpoint);
|
||||
|
||||
// IP address based access control
|
||||
InetAccessHandler accessHandler = new InetAccessHandler();
|
||||
for (String pattern : Settings.getInstance().getUiWhitelist()) {
|
||||
accessHandler.include(pattern);
|
||||
}
|
||||
this.server.setHandler(accessHandler);
|
||||
|
||||
// URL rewriting
|
||||
RewriteHandler rewriteHandler = new RewriteHandler();
|
||||
accessHandler.setHandler(rewriteHandler);
|
||||
|
||||
// Context
|
||||
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
|
||||
context.setContextPath("/");
|
||||
rewriteHandler.setHandler(context);
|
||||
|
||||
// Cross-origin resource sharing
|
||||
FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class);
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE");
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");
|
||||
context.addFilter(corsFilterHolder, "/*", null);
|
||||
|
||||
ClassLoader loader = this.getClass().getClassLoader();
|
||||
|
||||
// Node management UI download servlet
|
||||
ServletHolder uiDownloadServlet = new ServletHolder("node-ui-download", new DefaultServlet(new DownloadResourceService()));
|
||||
uiDownloadServlet.setInitParameter("resourceBase", loader.getResource(DOWNLOADS_RESOURCE_PATH + "/").toString());
|
||||
uiDownloadServlet.setInitParameter("dirAllowed", "true");
|
||||
uiDownloadServlet.setInitParameter("pathInfoOnly", "true");
|
||||
context.addServlet(uiDownloadServlet, "/downloads/*");
|
||||
|
||||
// Node management UI static content servlet
|
||||
ServletHolder uiServlet = new ServletHolder("node-management-ui", DefaultServlet.class);
|
||||
uiServlet.setInitParameter("resourceBase", loader.getResource("node-management-ui/").toString());
|
||||
uiServlet.setInitParameter("dirAllowed", "true");
|
||||
uiServlet.setInitParameter("pathInfoOnly", "true");
|
||||
context.addServlet(uiServlet, "/*");
|
||||
|
||||
rewriteHandler.addRule(new RedirectPatternRule("", "/index.html")); // node management UI start page
|
||||
|
||||
// Start server
|
||||
this.server.start();
|
||||
} catch (Exception e) {
|
||||
// Failed to start
|
||||
throw new RuntimeException("Failed to start node management UI", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
// Stop server
|
||||
this.server.stop();
|
||||
} catch (Exception e) {
|
||||
// Failed to stop
|
||||
}
|
||||
|
||||
this.server = null;
|
||||
}
|
||||
|
||||
}
|
@@ -5,13 +5,30 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public abstract class ExecuteProduceConsume implements Runnable {
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class StatsSnapshot {
|
||||
public int activeThreadCount = 0;
|
||||
public int greatestActiveThreadCount = 0;
|
||||
public int consumerCount = 0;
|
||||
public int tasksProduced = 0;
|
||||
public int tasksConsumed = 0;
|
||||
public int spawnFailures = 0;
|
||||
|
||||
public StatsSnapshot() {
|
||||
}
|
||||
}
|
||||
|
||||
private final String className;
|
||||
private final Logger logger;
|
||||
private final boolean isLoggerTraceEnabled;
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
@@ -24,12 +41,14 @@ public abstract class ExecuteProduceConsume implements Runnable {
|
||||
private volatile int consumerCount = 0;
|
||||
private volatile int tasksProduced = 0;
|
||||
private volatile int tasksConsumed = 0;
|
||||
private volatile int spawnFailures = 0;
|
||||
|
||||
private volatile boolean hasThreadPending = false;
|
||||
|
||||
public ExecuteProduceConsume(ExecutorService executor) {
|
||||
this.className = this.getClass().getSimpleName();
|
||||
this.logger = LogManager.getLogger(this.getClass());
|
||||
this.isLoggerTraceEnabled = this.logger.isTraceEnabled();
|
||||
|
||||
this.executor = executor;
|
||||
}
|
||||
@@ -51,28 +70,19 @@ public abstract class ExecuteProduceConsume implements Runnable {
|
||||
return this.executor.awaitTermination(timeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public int getActiveThreadCount() {
|
||||
synchronized (this) {
|
||||
return this.activeThreadCount;
|
||||
}
|
||||
}
|
||||
public StatsSnapshot getStatsSnapshot() {
|
||||
StatsSnapshot snapshot = new StatsSnapshot();
|
||||
|
||||
public int getGreatestActiveThreadCount() {
|
||||
synchronized (this) {
|
||||
return this.greatestActiveThreadCount;
|
||||
snapshot.activeThreadCount = this.activeThreadCount;
|
||||
snapshot.greatestActiveThreadCount = this.greatestActiveThreadCount;
|
||||
snapshot.consumerCount = this.consumerCount;
|
||||
snapshot.tasksProduced = this.tasksProduced;
|
||||
snapshot.tasksConsumed = this.tasksConsumed;
|
||||
snapshot.spawnFailures = this.spawnFailures;
|
||||
}
|
||||
}
|
||||
|
||||
public int getTasksProduced() {
|
||||
synchronized (this) {
|
||||
return this.tasksProduced;
|
||||
}
|
||||
}
|
||||
|
||||
public int getTasksConsumed() {
|
||||
synchronized (this) {
|
||||
return this.tasksConsumed;
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +101,8 @@ public abstract class ExecuteProduceConsume implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName(this.className + "-" + Thread.currentThread().getId());
|
||||
if (this.isLoggerTraceEnabled)
|
||||
Thread.currentThread().setName(this.className + "-" + Thread.currentThread().getId());
|
||||
|
||||
boolean wasThreadPending;
|
||||
synchronized (this) {
|
||||
@@ -126,10 +137,9 @@ public abstract class ExecuteProduceConsume implements Runnable {
|
||||
this.logger.trace(() -> String.format("[%d] producing, activeThreadCount: %d, consumerCount: %d, canBlock is %b...",
|
||||
Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount, lambdaCanIdle));
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
final long beforeProduce = isLoggerTraceEnabled ? System.currentTimeMillis() : 0;
|
||||
task = produceTask(canBlock);
|
||||
final long delay = System.currentTimeMillis() - now;
|
||||
this.logger.trace(() -> String.format("[%d] producing took %dms", Thread.currentThread().getId(), delay));
|
||||
this.logger.trace(() -> String.format("[%d] producing took %dms", Thread.currentThread().getId(), System.currentTimeMillis() - beforeProduce));
|
||||
}
|
||||
|
||||
if (task == null)
|
||||
@@ -167,6 +177,7 @@ public abstract class ExecuteProduceConsume implements Runnable {
|
||||
try {
|
||||
this.executor.execute(this); // Same object, different thread
|
||||
} catch (RejectedExecutionException e) {
|
||||
++this.spawnFailures;
|
||||
this.hasThreadPending = false;
|
||||
this.logger.trace(() -> String.format("[%d] failed to spawn another thread", Thread.currentThread().getId()));
|
||||
}
|
||||
@@ -193,7 +204,8 @@ public abstract class ExecuteProduceConsume implements Runnable {
|
||||
} catch (InterruptedException e) {
|
||||
// We're in shutdown situation so exit
|
||||
} finally {
|
||||
Thread.currentThread().setName(this.className + "-dormant");
|
||||
if (this.isLoggerTraceEnabled)
|
||||
Thread.currentThread().setName(this.className);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorCompletionService;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.net.ntp.NTPUDPClient;
|
||||
import org.apache.commons.net.ntp.NtpV3Packet;
|
||||
@@ -18,7 +19,6 @@ import org.apache.commons.net.ntp.TimeInfo;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class NTP implements Runnable {
|
||||
|
||||
@@ -53,15 +53,10 @@ public class NTP implements Runnable {
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
public boolean doPoll(NTPUDPClient client) {
|
||||
public boolean doPoll(NTPUDPClient client, final long now) {
|
||||
Thread.currentThread().setName(String.format("NTP: %s", this.remote));
|
||||
|
||||
try {
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
if (now < this.nextPoll)
|
||||
return false;
|
||||
|
||||
boolean isUpdated = false;
|
||||
try {
|
||||
TimeInfo timeInfo = client.getTime(InetAddress.getByName(remote));
|
||||
@@ -110,26 +105,26 @@ public class NTP implements Runnable {
|
||||
}
|
||||
|
||||
private final NTPUDPClient client;
|
||||
private List<NTPServer> ntpServers = new ArrayList<>();
|
||||
private final List<NTPServer> ntpServers = new ArrayList<>();
|
||||
private final ExecutorService serverExecutor;
|
||||
|
||||
private NTP() {
|
||||
private NTP(String[] serverNames) {
|
||||
client = new NTPUDPClient();
|
||||
client.setDefaultTimeout(2000);
|
||||
|
||||
for (String serverName : Settings.getInstance().getNtpServers())
|
||||
for (String serverName : serverNames)
|
||||
ntpServers.add(new NTPServer(serverName));
|
||||
|
||||
serverExecutor = Executors.newCachedThreadPool();
|
||||
}
|
||||
|
||||
public static synchronized void start() {
|
||||
public static synchronized void start(String[] serverNames) {
|
||||
if (isStarted)
|
||||
return;
|
||||
|
||||
isStarted = true;
|
||||
instanceExecutor = Executors.newSingleThreadExecutor();
|
||||
instance = new NTP();
|
||||
instance = new NTP(serverNames);
|
||||
instanceExecutor.execute(instance);
|
||||
}
|
||||
|
||||
@@ -137,9 +132,9 @@ public class NTP implements Runnable {
|
||||
instanceExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
public static synchronized void testMode() {
|
||||
// Fix offset to match system time
|
||||
NTP.offset = 0L;
|
||||
public static synchronized void setFixedOffset(Long offset) {
|
||||
// Fix offset, e.g. for testing
|
||||
NTP.offset = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +146,7 @@ public class NTP implements Runnable {
|
||||
if (NTP.offset == null)
|
||||
return null;
|
||||
|
||||
return System.currentTimeMillis() + NTP.offset + Settings.getInstance().getTestNtpOffset();
|
||||
return System.currentTimeMillis() + NTP.offset;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
@@ -161,103 +156,120 @@ public class NTP implements Runnable {
|
||||
while (!isStopping) {
|
||||
Thread.sleep(1000);
|
||||
|
||||
CompletionService<Boolean> ecs = new ExecutorCompletionService<>(serverExecutor);
|
||||
for (NTPServer server : ntpServers)
|
||||
ecs.submit(() -> server.doPoll(client));
|
||||
boolean haveUpdates = pollServers();
|
||||
if (!haveUpdates)
|
||||
continue;
|
||||
|
||||
boolean hasUpdate = false;
|
||||
for (int i = 0; i < ntpServers.size(); ++i) {
|
||||
if (isStopping)
|
||||
return;
|
||||
|
||||
try {
|
||||
hasUpdate = ecs.take().get() || hasUpdate;
|
||||
} catch (ExecutionException e) {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdate) {
|
||||
double s0 = 0;
|
||||
double s1 = 0;
|
||||
double s2 = 0;
|
||||
|
||||
for (NTPServer server : ntpServers) {
|
||||
if (server.offset == null) {
|
||||
server.usage = ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
server.usage = '+';
|
||||
double value = server.offset * (double) server.stratum;
|
||||
|
||||
s0 += 1;
|
||||
s1 += value;
|
||||
s2 += value * value;
|
||||
}
|
||||
|
||||
if (s0 < ntpServers.size() / 3 + 1) {
|
||||
LOGGER.debug(String.format("Not enough replies (%d) to calculate network time", s0));
|
||||
} else {
|
||||
double thresholdStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
|
||||
double mean = s1 / s0;
|
||||
|
||||
// Now only consider offsets within 1 stddev?
|
||||
s0 = 0;
|
||||
s1 = 0;
|
||||
s2 = 0;
|
||||
|
||||
for (NTPServer server : ntpServers) {
|
||||
if (server.offset == null || server.reach == 0)
|
||||
continue;
|
||||
|
||||
if (Math.abs(server.offset * (double)server.stratum - mean) > thresholdStddev)
|
||||
continue;
|
||||
|
||||
server.usage = '*';
|
||||
s0 += 1;
|
||||
s1 += server.offset;
|
||||
s2 += server.offset * server.offset;
|
||||
}
|
||||
|
||||
if (s0 <= 1) {
|
||||
LOGGER.debug(String.format("Not enough useful values (%d) to calculate network time. (stddev: %7.4f)", s0, thresholdStddev));
|
||||
} else {
|
||||
double filteredMean = s1 / s0;
|
||||
double filteredStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
|
||||
|
||||
LOGGER.trace(String.format("Threshold stddev: %7.3f, mean: %7.3f, stddev: %7.3f, nValues: %.0f / %d",
|
||||
thresholdStddev, filteredMean, filteredStddev, s0, ntpServers.size()));
|
||||
|
||||
NTP.offset = (long) filteredMean;
|
||||
LOGGER.debug(String.format("New NTP offset: %d", NTP.offset));
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGGER.getLevel().isMoreSpecificThan(Level.TRACE)) {
|
||||
LOGGER.trace(String.format("%c%16s %16s %2s %c %4s %4s %3s %7s %7s %7s",
|
||||
' ', "remote", "refid", "st", 't', "when", "poll", "reach", "delay", "offset", "jitter"
|
||||
));
|
||||
|
||||
for (NTPServer server : ntpServers)
|
||||
LOGGER.trace(String.format("%c%16.16s %16.16s %2s %c %4s %4d %3o %7s %7s %7s",
|
||||
server.usage,
|
||||
server.remote,
|
||||
formatNull("%s", server.refId, ""),
|
||||
formatNull("%2d", server.stratum, ""),
|
||||
server.type,
|
||||
formatNull("%4d", server.getWhen(), "-"),
|
||||
server.poll,
|
||||
server.reach,
|
||||
formatNull("%5dms", server.delay, ""),
|
||||
formatNull("% 5.0fms", server.offset, ""),
|
||||
formatNull("%5.2fms", server.jitter, "")
|
||||
));
|
||||
}
|
||||
}
|
||||
calculateOffset();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Exit
|
||||
// Interrupted - time to exit
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean pollServers() throws InterruptedException {
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
List<NTPServer> pendingServers = ntpServers.stream().filter(ntpServer -> now >= ntpServer.nextPoll).collect(Collectors.toList());
|
||||
|
||||
CompletionService<Boolean> ecs = new ExecutorCompletionService<>(serverExecutor);
|
||||
for (NTPServer server : pendingServers)
|
||||
ecs.submit(() -> server.doPoll(client, now));
|
||||
|
||||
boolean haveUpdate = false;
|
||||
for (int i = 0; i < pendingServers.size(); ++i) {
|
||||
if (isStopping)
|
||||
return false;
|
||||
|
||||
try {
|
||||
haveUpdate = ecs.take().get() || haveUpdate;
|
||||
} catch (ExecutionException e) {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
|
||||
return haveUpdate;
|
||||
}
|
||||
|
||||
private void calculateOffset() {
|
||||
double s0 = 0;
|
||||
double s1 = 0;
|
||||
double s2 = 0;
|
||||
|
||||
for (NTPServer server : ntpServers) {
|
||||
if (server.offset == null) {
|
||||
server.usage = ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
server.usage = '+';
|
||||
double value = server.offset * (double) server.stratum;
|
||||
|
||||
s0 += 1;
|
||||
s1 += value;
|
||||
s2 += value * value;
|
||||
}
|
||||
|
||||
if (s0 < ntpServers.size() / 3 + 1) {
|
||||
final double numberReplies = s0;
|
||||
LOGGER.debug(() -> String.format("Not enough replies (%d) to calculate network time", numberReplies));
|
||||
} else {
|
||||
double thresholdStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
|
||||
double mean = s1 / s0;
|
||||
|
||||
// Now only consider offsets within 1 stddev?
|
||||
s0 = 0;
|
||||
s1 = 0;
|
||||
s2 = 0;
|
||||
|
||||
for (NTPServer server : ntpServers) {
|
||||
if (server.offset == null || server.reach == 0)
|
||||
continue;
|
||||
|
||||
if (Math.abs(server.offset * (double)server.stratum - mean) > thresholdStddev)
|
||||
continue;
|
||||
|
||||
server.usage = '*';
|
||||
s0 += 1;
|
||||
s1 += server.offset;
|
||||
s2 += server.offset * server.offset;
|
||||
}
|
||||
|
||||
final double numberValues = s0;
|
||||
if (s0 <= 1) {
|
||||
LOGGER.debug(() -> String.format("Not enough useful values (%d) to calculate network time. (stddev: %7.4f)", numberValues, thresholdStddev));
|
||||
} else {
|
||||
double filteredMean = s1 / s0;
|
||||
double filteredStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
|
||||
|
||||
LOGGER.trace(() -> String.format("Threshold stddev: %7.3f, mean: %7.3f, stddev: %7.3f, nValues: %.0f / %d",
|
||||
thresholdStddev, filteredMean, filteredStddev, numberValues, ntpServers.size()));
|
||||
|
||||
NTP.offset = (long) filteredMean;
|
||||
LOGGER.debug(() -> String.format("New NTP offset: %d", NTP.offset));
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGGER.getLevel().isLessSpecificThan(Level.TRACE)) {
|
||||
LOGGER.trace(() -> String.format("%c%16s %16s %2s %c %4s %4s %3s %7s %7s %7s",
|
||||
' ', "remote", "refid", "st", 't', "when", "poll", "reach", "delay", "offset", "jitter"
|
||||
));
|
||||
|
||||
for (NTPServer server : ntpServers)
|
||||
LOGGER.trace(() -> String.format("%c%16.16s %16.16s %2s %c %4s %4d %3o %7s %7s %7s",
|
||||
server.usage,
|
||||
server.remote,
|
||||
formatNull("%s", server.refId, ""),
|
||||
formatNull("%2d", server.stratum, ""),
|
||||
server.type,
|
||||
formatNull("%4d", server.getWhen(), "-"),
|
||||
server.poll,
|
||||
server.reach,
|
||||
formatNull("%5dms", server.delay, ""),
|
||||
formatNull("% 5.0fms", server.offset, ""),
|
||||
formatNull("%5.2fms", server.jitter, "")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
29
src/main/java/org/qortal/utils/RandomizeList.java
Normal file
29
src/main/java/org/qortal/utils/RandomizeList.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class RandomizeList {
|
||||
private static final Random random = new Random();
|
||||
|
||||
public static <T> List<T> randomize(List<T> inputList) {
|
||||
List<T> outputList = new ArrayList<T>();
|
||||
|
||||
Iterator<T> inputIterator = inputList.iterator();
|
||||
while (inputIterator.hasNext()) {
|
||||
T element = inputIterator.next();
|
||||
|
||||
if (outputList.isEmpty()) {
|
||||
outputList.add(element);
|
||||
} else {
|
||||
int outputIndex = random.nextInt(outputList.size() + 1);
|
||||
outputList.add(outputIndex, element);
|
||||
}
|
||||
}
|
||||
|
||||
return outputList;
|
||||
}
|
||||
|
||||
}
|
@@ -7,6 +7,7 @@ import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.Transformer;
|
||||
@@ -28,7 +29,14 @@ public class Serialization {
|
||||
// (At least until the BigDecimal XmlAdapter works - see data/package-info.java)
|
||||
byte[] amountBytes = amount.setScale(8).unscaledValue().toByteArray();
|
||||
byte[] output = new byte[length];
|
||||
|
||||
// To retain sign of 'amount', we might need to explicitly fill 'output' with leading 1s
|
||||
if (amount.signum() == -1)
|
||||
// Negative values: fill output with 1s
|
||||
Arrays.fill(output, (byte) 0xff);
|
||||
|
||||
System.arraycopy(amountBytes, 0, output, length - amountBytes.length, amountBytes.length);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"defaultGroupId": 0,
|
||||
"oneNamePerAccount": true,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
"maxRewardSharesPerMintingAccount": 6,
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 2592000000,
|
||||
"onlineAccountSignaturesMaxLifetime": 3196800000,
|
||||
@@ -57,21 +57,22 @@
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
"timestamp": "1580820000000",
|
||||
"timestamp": "1583870000000",
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT", "description": "QORTAL coin", "quantity": 0, "isDivisible": true, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC", "data": "{}" },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QY82MasqEH6ChwXaETH4piMtE8Pk4NBWD3", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QdAESUvjxT764zNnYyo53DVbu7nusmjuee", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QcPro2T97Q8cAfcVM4Pn4fv71Za4T6oeFD", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QbJqEntoBFps7XECQkTDFzXNCdz9R2qmkB", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "Qc5sZS1Vb1ujj8qvL5uXV5y5yQPq6pw2GC", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QceNmCiZxxLdvL85huifVcnk64udcJ47Jr", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "Qd453ewoyESrEgUab6dTFe2pufWkD94Tsm", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QfjoMGib4trpZHzxUSMdmtiRnsrLNf74zp", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "Qgfh143pRJyxpS92JoazjXNMH1uZueQBZ2", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "Qi3N6fNRrs15EHmkxYyWHyh4z3Dp2rVU2i", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QicRwDhfk8M2CGNvpMEmYzQEjESvF7WrFY", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QLwMaXmDDUvh7aN5MdpY28rqTKE8U1Cepc", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QP3J3GHgjqP69neTAprpYe4co33eKQiQpS", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
@@ -84,10 +85,29 @@
|
||||
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QTx98gU8ErigXkViWkvRH5JfaMpk8b3bHe", "andMask": -1, "orMask": 0, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QbmBdAbvmXqCya8bia8WaD5izumKkC9BrY", "andMask": -1, "orMask": 0, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QNuvYvBRUQgNj8u7gbLqBruDjKL1qzemND", "andMask": -1, "orMask": 0, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QSJ5cDLWivGcn9ym21azufBfiqeuGH1maq", "andMask": -1, "orMask": 0, "xorMask": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QhPXuFFRa9s91Q2qYKpSN5LVCUTqYkgRLz", "andMask": -1, "orMask": 0, "xorMask": 0 },
|
||||
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPFM4xX2826MuBEhMtdReW1QR3vRYrQff3", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVhboSLD1VmX2YvAnfAXkbzvsmXkDJZTNR", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QagyUZdmnKJA9LyEqcxJFFf2ehmcqVZsKb", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QcCYuXos5xBXXHbRg1RTfSdxiZEkGa3N2P", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qg3TKLhPvn7bKVrT9x37wJiJ7YZ4jBuqQW", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QiQUssukhoo1ft4G9Mxa8JpViqFW4PdBjJ", "level": 5 },
|
||||
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTVLRwoopfg7qSG9CMfCPJz3UydnT3jDxD", "level": 4 },
|
||||
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPLoqpwAoytvpQKwvJ6GRsaRcVZ3xnYgVB", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRPhK2FwijTaMQ4PeJ9gFK1vvGBaYaht2P", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRk5TG57SQGLkybXUqxBnobADTFGj9GR3Z", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTrosCDj7oCdJc8K7sJZW6ssHU3StVoY7s", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVgX5FVNvAvESmeVD67vz3xE7BQTQARLLM", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVxGkDgXt4nHj4MAd1afV9AxT1XCUVLGja", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXHmtFXzf4D7PEu73NfBm3sZyeuGrm3QC5", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qata5oApMShnD4F1kcgSJMTiYsxTPSFW4F", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QeoFPT4xPxLp2P42yKVo22wx2V8kydfNmr", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qi7Lbvz1UdWgzgbKzfpRisLsdLR4smVoVh", "level": 3 },
|
||||
|
||||
{ "type": "REWARD_SHARE", "minterPublicKey": "HFDmuc4HAAoVs9Siea3MugjBHasbotgVz2gsRDuLAAcB", "recipient": "QY82MasqEH6ChwXaETH4piMtE8Pk4NBWD3", "rewardSharePublicKey": "F35TbQXmgzz32cALj29jxzpdYSUKQvssqThLsZSabSXx", "sharePercent": 0 },
|
||||
|
||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "owner": "QbmBdAbvmXqCya8bia8WaD5izumKkC9BrY", "groupName": "dev-group", "description": "developer group", "isOpen": true, "approvalThreshold": "ONE", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 },
|
||||
@@ -95,381 +115,406 @@
|
||||
{ "type": "JOIN_GROUP", "joinerPublicKey": "Gqcfa6YGXnKTr4F3QMpDQkjxxLGBmsq83wXcYeZvES7i", "groupId": 1 },
|
||||
{ "type": "ADD_GROUP_ADMIN", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "member": "QTx98gU8ErigXkViWkvRH5JfaMpk8b3bHe" },
|
||||
|
||||
{ "type": "JOIN_GROUP", "joinerPublicKey": "CrHdj5wKFtu7961zYvCxW76K1hHPSNpo43XP1hTWryJR", "groupId": 1 },
|
||||
{ "type": "ADD_GROUP_ADMIN", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "member": "QNuvYvBRUQgNj8u7gbLqBruDjKL1qzemND" },
|
||||
|
||||
{ "type": "JOIN_GROUP", "joinerPublicKey": "2ZnSnZcpvMePw23f3AefANbRQHs7KjDyAyEzXc9dBMdn", "groupId": 1 },
|
||||
{ "type": "ADD_GROUP_ADMIN", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "member": "QSJ5cDLWivGcn9ym21azufBfiqeuGH1maq" },
|
||||
|
||||
{ "type": "JOIN_GROUP", "joinerPublicKey": "25jxnXz3xF3tK2WJHpUHwGx7DXR6yfjAHF1hFX5ice6H", "groupId": 1 },
|
||||
{ "type": "ADD_GROUP_ADMIN", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "member": "QhPXuFFRa9s91Q2qYKpSN5LVCUTqYkgRLz" },
|
||||
|
||||
{ "type": "UPDATE_GROUP", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "newOwner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT20", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 },
|
||||
|
||||
{ "type": "REGISTER_NAME", "registrantPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "owner": "QbmBdAbvmXqCya8bia8WaD5izumKkC9BrY", "name": "ciyam", "data": "{}" },
|
||||
{ "type": "UPDATE_GROUP", "ownerPublicKey": "CVancqfgb2vWLXHjqZF8LtoQyB7Y5HtZUrFKvtwrTkNW", "groupId": 1, "newOwner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT40", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 },
|
||||
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVLuvt9krmxXwQPAeAhxzhuMF5i8F4aNs8", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qe9VPzQp3h4Kg3DHSHBUQ3AM3AiRBfCDfX", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QcrnYL6yNwHKuEzYLXQ8LewG3m2B5k9K5f", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPYfRd1uhnAgqkZNmjNCjgPhkguMnHWuc4", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QM9zVbXXnfrtQ1X7zPQ5zxPYPAWTaVMXqZ", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QcHF9YogbuzZhG4fK4116pgE2qrmbkGh2n", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qft1ktvJ14eBFjpJaphT24ks4WRcN3K6tB", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QaUciVnbQDXdNygJadEY31PuDEBLi6Spmu", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QcHF9YogbuzZhG4fK4116pgE2qrmbkGh2n", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWe1iPDudLU189BggPykbH1DrAeaFEgX6W", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgECFJiiri2dDN4zA32URvbdDid2cFrJwM", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTTrv8SWR8huV8TFYUEQhfZ1j1JmtL5p8G", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QYgVi26jUqMzJo4ahZV9yekQNnYKHBaX8r", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQa3MTgdnru5B7wSqPcq7qXcZcpbDQ7oyE", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXan6JJ1WuRi7GigvFDjtTzJY1rfYEqEqv", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQa3MTgdnru5B7wSqPcq7qXcZcpbDQ7oyE", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQoHo1x4hvZcSFbj2uQAXQhq2ZXBUgPeNj", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQEZEGWt3sAPwEWYD2RQ6tMwnpkayG81dY", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QUvoLFfkuVuRe1KGMLQS4nUHry6CBTuTYz", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTEE4ZJXv68ke4841HWjTLAAU8mfccxwbE", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQoHo1x4hvZcSFbj2uQAXQhq2ZXBUgPeNj", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVurebcEbe4USR4xcS3Mbk12mhxsjRX31u", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QfmM8dgfikTB2FYVuJ9owzQXVm8wP7T4QT", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPUMyJ59kkrp75tDzDPxSyw1GWCrbC2cS2", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQEZEGWt3sAPwEWYD2RQ6tMwnpkayG81dY", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qc9dZchoYfc1eRJhSLXR9rxSHcqNB47Dex", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTMTFswUU83XVmk6T4Gez7qUJCccbAad7S", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QjrC8NXwR8gFkEauvRwCPxqHroPFqAJbhK", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPcTWoAhYWmwjmWbQAS8muisrQVaLJMbg7", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWpJxitn53ovwknQfrCaqivvoUNuPrX2sb", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QLpLY9o3X21q7q4L6u5JdDuMskYA838iYE", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QdGbhtkFHUqd9nK9UegxxGXD1eSRYSoKjt", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QjrC8NXwR8gFkEauvRwCPxqHroPFqAJbhK", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXv4WiqgryFPi8BgX7RU7gqtAgrVutmU4S", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QSkiGy3v3MwvGy3aACuyLDv3Xy2AWAYfPS", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgpAW6uqwNR58gNYWRCVXNLm4F5TuckAw4", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QLpLY9o3X21q7q4L6u5JdDuMskYA838iYE", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QguNJ4evpN9i76dYHysTzLiKoWKFhE4B4U", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWpJxitn53ovwknQfrCaqivvoUNuPrX2sb", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QdGbhtkFHUqd9nK9UegxxGXD1eSRYSoKjt", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QP8xG56L8b28h1mguSk9LuzNhxbHgAoL9b", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QfjL32jLsxtumbfx6ufmfCFCBccVCQFkrh", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWLpsGYrkF2cy3tH6DCxso7kXZpZJvv13e", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbwgBD6LWdk1hZsb8EwdVVmDZdvpxMzyGT", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qbcy4uyMkQF2JXYqGkueDiFNZ4tHjRg8CR", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QdXe21sjY8smjVmiAUgZY8xWVzwgxMgK5A", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QawB5MesBratjs2d9EMnXnrN4EC7gw7LRw", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPEbvVBWDG7qgy4smY8nWiie78Vec8qiT9", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXKmtkHHwaUQzGeHHG2dFiHUnKAp815Mzq", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QguNJ4evpN9i76dYHysTzLiKoWKFhE4B4U", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZkThgFfExognAbxLjYZGVHpL7X6g3EG4A", "level": 5 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZkThgFfExognAbxLjYZGVHpL7X6g3EG4A", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbH8srDKeS5VcsQsgsaF3nqCzGT1NqfsTx", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QdtAQm1EGNgM7QDSaC2qvV9WdpRHwpApUT", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbqRyFw7Xu6Nsb4FraaUSe7nUPukuUpekG", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRFHr4jnVgvAsPTubeSrh8bPy1yzwzYaWD", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWuW2YMygVtWieUo6a4yayD1xFDWdnmo5j", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbqRyFw7Xu6Nsb4FraaUSe7nUPukuUpekG", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QSkiGy3v3MwvGy3aACuyLDv3Xy2AWAYfPS", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVkQTr8PzDTaNxhvrC47UUxUi6V987a5qL", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQPYyoE3Bm2vh8Wr5aaBNyirC8dd3BhBGH", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QUzUCfoakDqBaL5zBgfvTKLHcuxbUfB38Q", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QSBJuNoCAFUTcevuCTcMi3i5nzNPhC5R4b", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTGeQqn3XEFdnnCqvifCFXYdKym7SaHzTd", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVSqUrNFR4mPTMa7UdVmNKZTSaDVAv8XXF", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTGeQqn3XEFdnnCqvifCFXYdKym7SaHzTd", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QNJ8jDx6Mni2GKLHEY1BMh9xDumT3vnJQM", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QMLjFiPiPa46YBRoushe6a227kSrXnXyKb", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgVZb632eqF1eLQm9gBGuBtyp9Dyz2FKUK", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPREQjU2defiYdgA33HDiLNGBpxtuebeqE", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QiBYApdEYRwsFYjt59UJqZV55wcwykvhsh", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QNJ8jDx6Mni2GKLHEY1BMh9xDumT3vnJQM", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QMsKXQAYKmR7dBH4P3kMLiKzYatK3h1CeS", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRCtc67FTNKS5zVXM8omw8F55h9DP7herL", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZvHW7amu5DNktsBgaMrR1brHZhhhVwKLW", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRCtc67FTNKS5zVXM8omw8F55h9DP7herL", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgqM5bKs3tNqKNAnVeaQp4oaYMXCmX6YJr", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QhMEWhZCufhKLkfuNU2DAzj1mmWoAxX147", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qgwb5SLVGAperXAMoVNBDvGpVAxTVY7f6F", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbvxC3ENqomXp11833APchdjeyCNd49nLj", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qj5ncwncQY4KPYgKHD1eYpXHbR717PeLcJ", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QY1RFZTD2ogRohf3UrdT4g1Qo9D122AZDN", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QcU4VhU9ohDXU4k4AUMapgJRYSzEpizjLN", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QY1RFZTD2ogRohf3UrdT4g1Qo9D122AZDN", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZsygC1chppsJK1cnaHG4fEsNaDYfLF7ZJ", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QYGcPZcRhGaY1MsiDr3VtwTXmB9TAbLFSn", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QaFN7DWcGF7keNACJpwCVnegePbfsAoFCw", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QYGNMWBmqWgVtMWGHypAsKhDVQw5mrFZww", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QcNmqT8CZ6zSZwuRm5LahRZnuGBJRnPY8o", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXv4WiqgryFPi8BgX7RU7gqtAgrVutmU4S", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRKRk5HVADsN1LHygK7q2pA7dWnYKnPpCT", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QSGB4Rd2xhd6UmA9LALTQ4f89Tfsz5VajU", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QNVKrjEq5bZdiDtgo64m5kz87rTHqCwvCP", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QNMDKE7XTujNQkuQorcHXw6hL7qRvyaTjr", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qgwb5SLVGAperXAMoVNBDvGpVAxTVY7f6F", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgCQq4cFaGrJhwvKs4XwvccKiLZ8GVMCXR", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRtRELSSASzqiYy2FtNcrePH6TVnqJkv9B", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QMtm8wVPHGE3qHg2hMaj6SZ78D5eXw3VWZ", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QY6ZGZdi8h5op2VrRXkG1W5Jp3feLwp7ZD", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QY6ZGZdi8h5op2VrRXkG1W5Jp3feLwp7ZD", "level": 4 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVrvy4ac2jBTfxyCKB7MLimqJooTDBApmS", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QcCBVfL35rxSyQ416L2MBz14FYbNrbeNPx", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QNHdGeFJmPcDdN8prPzPL4bk2dpnJ2ZZFr", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRWEbcRnLoGccAndtLcGgpeQFH2ZBcMqHo", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QcCBVfL35rxSyQ416L2MBz14FYbNrbeNPx", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qce2Djqrk2WzG1QhMZ3BqFok9HGsz4wtM3", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRWEbcRnLoGccAndtLcGgpeQFH2ZBcMqHo", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QeDhJRqTyeDmcgGJoms6FHK49ZGVpVahxg", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QUXga5K8nzd9EqYtvEesZWEYuA688h6D3d", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QfbX8JJupEw5ckNtU4upQgET35oLTr5e6v", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRaDef6H2zYfefqLwYGmUg7T6DAqo6DDqc", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QibuD4c6gvXgS4iut7q3sXuVb23rgFJq2M", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QawSgZ7i2LLFTKyPxQptk9gN526ihy5yZi", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QMozpRT9aUunfmPh7EtQ6LPoth2JFJWBXC", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTpYQqRyMekaEuECziirzy3HvCVofZS1wJ", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QawSgZ7i2LLFTKyPxQptk9gN526ihy5yZi", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QMuWNAJ2tbeViHtBUN3yD2KARrrzcanLAd", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QUGo9SErgc6ceB5aBzcSJDNqBkQ9eaCKZS", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QdFZk74skMUu4rKMPEmcSVwR87LNDe6o3Y", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWLpsGYrkF2cy3tH6DCxso7kXZpZJvv13e", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qgp16aMcdiS2EUkxCm5NSZgB8DixGK51zT", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QScBgSw74MquesXmVJxerX3YgyhtShRr4q", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgcF6KgVZ9eDAMHJdSEeAtp91t931VKZMv", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QjgGeEkyiXa43pyqkXxZbvAChQpVYfUyKz", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRWEbzH4niUcu9dL3Yq42X4j89aqQk3qWw", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QYD3kXchZ86vUyJBXNCVQ4LUvTAd6PUZW3", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPqfuZpmyA6cK6WUFwcGeKH2Te1aegkHBM", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QhQdzLn36SDgrgoMfvdZAkoWtTUHpB3acJ", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgcF6KgVZ9eDAMHJdSEeAtp91t931VKZMv", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRWEbzH4niUcu9dL3Yq42X4j89aqQk3qWw", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWL7kZp6Pdd1bhxZ6SXPhVf5g7GParG9CC", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbxJvwrEHZs7MDE8rbqBwZAZkcywue5F3W", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QhQdzLn36SDgrgoMfvdZAkoWtTUHpB3acJ", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPqfuZpmyA6cK6WUFwcGeKH2Te1aegkHBM", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QUKKwug9PNai3DBggXUXP8Ag7WmR5SVUR4", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWwrtjBL4ah965XPXHYJhymreC9jyryNLZ", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QjEaMxcBKMsj91ytKe6GdTBJP8Mu1Ru3r4", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbYTowTHCr9WzfrR6b8uDfJKwL41nG1vyr", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPusqAVBVFGAAeE7RdospttA18AuyLP7sB", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qd1Px9vhWuEmF2SbLx3Ez7HhGtifGMa8TJ", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QSq8y4ZrSbF55ZddWNcw1ett2LDtjQEvNn", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qd1Px9vhWuEmF2SbLx3Ez7HhGtifGMa8TJ", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbYTowTHCr9WzfrR6b8uDfJKwL41nG1vyr", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QU8XJyQEZxSHgrS1XyooWUo5MmruXg36Pe", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QNw9xAm9TUerin9QsapCPL9mV6zmoXyJrh", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QYuMATkLjz7YB6s4EG1aWCmmmrAPj3W9Ce", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QNiTnonHpXTeUrgNdyYWVDPP4ZdjkLpW72", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTtXS6fZGThRLq4qgkwM4ngBYkLoFyZ3bK", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QjrCFCi6dqvka4UELg2SHhM2oWnQWepd1o", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QYuMATkLjz7YB6s4EG1aWCmmmrAPj3W9Ce", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QREtYDhP4HkpeCCZroemuGXMGVFoZHH3Lp", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgesuKa3zwx8VAseF1oHZAFHMf29k8ergq", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QjrCFCi6dqvka4UELg2SHhM2oWnQWepd1o", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTtXS6fZGThRLq4qgkwM4ngBYkLoFyZ3bK", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QjEAs2or122weKppv5zALzoQzXxbsDjy3f", "level": 3 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QdXdUxnyKGGo7eEfTcx85oEikNe5nYnuwa", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgesuKa3zwx8VAseF1oHZAFHMf29k8ergq", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWBFK5h61ZxGfqQpEkwwKTcLAo8t9VWe4K", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QjEAs2or122weKppv5zALzoQzXxbsDjy3f", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZ19JRpSsgvm4z6EjnbhdxJBoUYzDGvP3x", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QUZQPWhrxpze32vGiux6wa85kg9iwuhCDx", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZ19JRpSsgvm4z6EjnbhdxJBoUYzDGvP3x", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QdwSxr3t4hdGHjQFy6EVGR9yGMipefsTuo", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZ2gi6BhUNpGmrErgJLFuY1WHy6xK1J7qX", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWinRb65f2g3yBoaZvTrQKQk7CW7vfBgGX", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQaKBSjAt9RK2bqJoSriR77X4ULstGzrFQ", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QU8XJyQEZxSHgrS1XyooWUo5MmruXg36Pe", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QisSQZ7Et7Rfzx2SCC2o9UDSeRZWMyFKWc", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWinRb65f2g3yBoaZvTrQKQk7CW7vfBgGX", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXYk68x2tiUrDBv8eq6wd4KtBmLHYiC4zR", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QisSQZ7Et7Rfzx2SCC2o9UDSeRZWMyFKWc", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTyokTJrR4b2y76An3BFUEbqQy5vvg76iN", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPyx2bNiAnJEjitfeAh8jZXzQVKio2B7Mi", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QMJwdufHY9dMoARHCUyGbMPAqUB4BcqGKm", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QNffmqjCUQsLaHLZXBaA47tQW7bERdpP4R", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QeUc5NfzubZD4eA4eJ7bfXAFraAWc7jVz5", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QeDhJRqTyeDmcgGJoms6FHK49ZGVpVahxg", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QfPcwetW3BErP4ySTurxFJSHpNkNXPEhGk", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QMMh94Pfs5LVE4xJee1yggViqP1YDdQHT4", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQHWFSJpzuDupPfcTvGMRNJp2UGz98Kb7j", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QdepeWWLMD3LiDRKkKBria29rDhZP9V5RJ", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QeSh3t1AnaRcRThkkUTvvdMEouixCADeVh", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWez1VBfYVJ8KFoZ6MhJDzYVLbn5mr38VT", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZiYh4m4Uh3FH52cnow8MrNyXhSH88bp2H", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QYsh2NB6TogqV1iXHmHXcVaWw25WEYA94o", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZjCgcSVvSRsFZeLJz9C5dTa36s3cSKqvB", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QfhJM5CpX5MhfjCcopwfw7pgS6w1hVJ49E", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QMMh94Pfs5LVE4xJee1yggViqP1YDdQHT4", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QU9dnN47Nc5KaH7JwNoCZ7TANwCW5VX9iG", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXoW5M113C3SDUeFdjVVs2m1RT9XtzXS3z", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QfhJM5CpX5MhfjCcopwfw7pgS6w1hVJ49E", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZjCgcSVvSRsFZeLJz9C5dTa36s3cSKqvB", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QjsD7pMh2LpmgYudaaRM8gmzhBC4c9uwyj", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZxcntxfJimHus9pgfvVVPbpHs5yU7ZMhB", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRBVE5gHumH6RUgEUxJdQ5417NUvc14k3F", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QakZtV3nZeRp5UEoEUNX7p8Qz5VbVXTQid", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXeiJa7ftN6dAYNZcdqKDBhmAFyGjX2zkm", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qc2eRuYXtpATrg87Pr1WDXkUBCsgNRYcQF", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbEJ5tfeskASVnmLFeKzspuZJJb6cJVPZ4", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWvTdm9LU1GSX9q6Rrvgx7xjo2iuV2Gxn1", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qc2eRuYXtpATrg87Pr1WDXkUBCsgNRYcQF", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QYpYshfVzMMVU71KqAV4erX3NJAUCnJXgD", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QViKVZa3M3ar7RBRSBMTx8FdzLh1zxUhN8", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QMb2cAKb2BUxknneuoXQynJ7uzosJ57Top", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZw7tgMttSySNMKfcMrEbdtnqHVrQ9w9fT", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbGwYRV3UDek4VNpzoAVVQWfoKjZmm4qPb", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWvTdm9LU1GSX9q6Rrvgx7xjo2iuV2Gxn1", "level": 2 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QYM76r3tuf6FJWz2USqVjwXJrV9tLMz1cj", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QfnbnWrRQ4HNDQvtg3wG2B1eC4ycUsFqZz", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QUdjqijDoyc83K4WcMW1sCn7zLd2t1WTqn", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QbGwYRV3UDek4VNpzoAVVQWfoKjZmm4qPb", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QRUbzEbLd7fRjAx2fBdXAH4QS1WQyetvDc", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QU9K6pWhkok9mokkPDoCwgSu3j62ECuKLD", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QaZs97g4Mbq9tXMoBWbhw3jFvBBVkWKS5F", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QcJwVCyzraPy51uB4xd4f94n2UFYAsznGC", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZbKMgYBwWGyTDJzoDir2aNqXsFdDp6fS9", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZ7wvWAUcHKRhvQ3ijdrqM4zucQKCgQ1hQ", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgmEtScSZWJmTUAidCZKj6gDr3LznZ6rr4", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQXgH4CnQCB76BbXhsApu6ShhohFfvoXv7", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZJc1V32oFm8tufB4bk7fa3aepu4EdkeDU", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZbKMgYBwWGyTDJzoDir2aNqXsFdDp6fS9", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QaZs97g4Mbq9tXMoBWbhw3jFvBBVkWKS5F", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QSkicapNH35a3UebSxxSMCfntBhwwi6veW", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QLqZD8eiNk8nrzyWcURfQDS9T5NWx98vrz", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQzMut6erjgSKCpZ1dHDcjKcj9KAce7cug", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QUdjqijDoyc83K4WcMW1sCn7zLd2t1WTqn", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QfnbnWrRQ4HNDQvtg3wG2B1eC4ycUsFqZz", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QU9K6pWhkok9mokkPDoCwgSu3j62ECuKLD", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QcJwVCyzraPy51uB4xd4f94n2UFYAsznGC", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVuksgNt3QAr7KCrkxtE5FWrczfgLKxs4H", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZJc1V32oFm8tufB4bk7fa3aepu4EdkeDU", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZ7wvWAUcHKRhvQ3ijdrqM4zucQKCgQ1hQ", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQXgH4CnQCB76BbXhsApu6ShhohFfvoXv7", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXpCzKnANyh8jrRRxemvsXHAeFRHrDvdXF", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qd1Xw41BzN1CgASqsh2PcrrkTKyDs2MVYF", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVEfMEeEHP768c9rEyi3WcH2JQwqheVDe3", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QehM6hQZVkrYAst8WrBVdQiMFfTtDtbKQu", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QY7gQwZKiPYMHYjYASSDBhhAoyYaxmex17", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXqAtZJsLU6HFGTzuegjeUbmEW8cGEnYt6", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QLqZD8eiNk8nrzyWcURfQDS9T5NWx98vrz", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QPE3EGR8LTYSLtWBq52SFte66oud6cxchF", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qa9NXeLAvmaTsKackfDRmm1A9zy7JeEjgV", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXqAtZJsLU6HFGTzuegjeUbmEW8cGEnYt6", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXpCzKnANyh8jrRRxemvsXHAeFRHrDvdXF", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qd1Xw41BzN1CgASqsh2PcrrkTKyDs2MVYF", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVb7P596imucSwDqb5HHEjqpnAre615PbS", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZgDpQ1Vyi5pNNe2ZzBRhcUgSU13yeWvmH", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QTgfJFaEj5wuFvufwjUmYPhQ8eVfHR4u22", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QNMtHRjEWPgZUVCpiC2qE2LyJsxq1BC1Yj", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QUNTxxwr9f47RcwY48ZUP6FmmKnWCsxes5", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QiQernJdiP4zCECB93v247FUtovkfJyTYm", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QSTDdguUfKe6TaQrik2zq4Xrbu6unxNa9o", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVb7P596imucSwDqb5HHEjqpnAre615PbS", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQfFiVWkyAmfs1hDUcWU6XBfonxLdz6RSN", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgsxpySDm1qja6D2EyKuHPiUhyTM1RMk6c", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWZwi5NUzHuJfg5fh9HzjWRpQc1fmMknAh", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QMb2cAKb2BUxknneuoXQynJ7uzosJ57Top", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QVuksgNt3QAr7KCrkxtE5FWrczfgLKxs4H", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QP9vU5yTsBjuTSFxH5Cb9VXYNRHKhMNAJ4", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QXm5e16Lq6dnYwpZJ8Rn2cME3ziHZfRRnp", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWkLaA4fYK51m4CszSgBd7efoDsSKamdf1", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQzMut6erjgSKCpZ1dHDcjKcj9KAce7cug", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QZgDpQ1Vyi5pNNe2ZzBRhcUgSU13yeWvmH", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QiQernJdiP4zCECB93v247FUtovkfJyTYm", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QUNTxxwr9f47RcwY48ZUP6FmmKnWCsxes5", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "Qa4KygekpW1Brr3D66K6rGtS5AY8qkhvD3", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QQfFiVWkyAmfs1hDUcWU6XBfonxLdz6RSN", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QViKVZa3M3ar7RBRSBMTx8FdzLh1zxUhN8", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QgJDnJF1zfoFBxB1LRvi5Rxb8p345UmVos", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QWZwi5NUzHuJfg5fh9HzjWRpQc1fmMknAh", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QNMtHRjEWPgZUVCpiC2qE2LyJsxq1BC1Yj", "level": 1 },
|
||||
{ "type": "ACCOUNT_LEVEL", "target": "QP9vU5yTsBjuTSFxH5Cb9VXYNRHKhMNAJ4", "level": 1 },
|
||||
|
||||
{ "type": "GENESIS", "recipient": "QS2k8PMpmvUHzFDp2JfbVxRo8SiGiV72xx", "amount": "637557960.49687541", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUM95jekFMye3Lmkg2gj7CdhFreYye4DaA", "amount": "400671036.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QaYAUoT2SzYA6kqBEW6W11brkYpSf7g4Wy", "amount": "352652351.30705076", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdoLZqB3pY4NgbSpvbsFzBDq2LcShjXsoq", "amount": "263574650.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTnPQVphizFhBFtSbTQzAC4Kubj4uqpv96", "amount": "232356835.62157121", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMtx2UmUuRZckCmRJRyxdzSAazHP8hU5rA", "amount": "160672815.43629771", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVz1rKM9QQrGX7gkxR9EV8a2T4AuD1QT87", "amount": "128506517.58266672", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUUcETWLi7EcjQ9M83Aur3SwmESFBFuQye", "amount": "126998534.24906898", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qa15fMdEiY7WqK2xwvdHrdhU2TE6yk3V3L", "amount": "122690068.33700012", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QM4LF9EzQnXJ9VyFwnJbJzskJLrybuSzsw", "amount": "114361793.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQKeokRiFCgAhBSdu1DUf5e1LCkgApvrxZ", "amount": "100000078.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QiaaoNZ54wKoaUMXxW72UsPt1MiPpeUTWm", "amount": "100000031.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYoRWAxw6CVMeYeWHKJh3csmTVkVzjpdBo", "amount": "100000026.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPwLkxemtGsrdcPJSA4iMgKbBPdecL7dwZ", "amount": "99999999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMkgf9Y6Ac2TUrynDvyhX69ekpC3P3GQmN", "amount": "99008835.47860426", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUnfm6t9SrhgFB8YqCV1vQhmqFzhncxioK", "amount": "80000207.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWZm17rRXeUehcM4TprVNNRSTHWQmG2bME", "amount": "62663714.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXdcbgN6vdQRBKJzHSB2JaSDDzPEsTUi14", "amount": "49450000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QczMrVLEnEAXwvucwrfasH9H7QHNMPxLKu", "amount": "46488501.53473695", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qaa5hs9dj1gPxhqigH7tVyZ3oRuVcMULXB", "amount": "44766868.70000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQMswzmv65MZSdafqzCACm3tbLj23ixkVR", "amount": "42447489.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qa95hURaNK4kPhDhbdmDFm2wMkkoWFZ4Zz", "amount": "40976709.97984710", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVEde9kJt2ejqXavBzK35zY3eztnkSXTwN", "amount": "38172492.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZjDFrMrgeR9SGL8bprqyiXVtAZMVCXESZ", "amount": "37291276.42323117", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPmmPjdDXW35ykF557KbaVy5jYBbaM3pRp", "amount": "33333332.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QaWReiLe5tAsTNoq7hMiSGNasJEXTX5bmn", "amount": "31465829.00562504", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLrErVJvpRdYAt4dGwk8i8rG3PMUSgpp3i", "amount": "26593505.67231102", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYsLsfwMRBPnunmuWmFkM4hvGsfooY8ssU", "amount": "25303386.66951814", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QgULJXk3qyLDr4EKkz48rxwwvWK7BDL8Ux", "amount": "22476856.86000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNwedkYs6hToBRPZvBb6naKvDEgB7BeaKi", "amount": "21967101.30582773", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QgP7vQABSDJwuE8VTdVkiNnbkorqWWctQm", "amount": "20442665.63310002", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QM5Fg3kBzj9BqsGAJKHhsEtgRgf4x2Yn5G", "amount": "20000006.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLdC7wm9PYC1KjgZaeVdGzH4M88BYhEeEw", "amount": "19927901.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLgBTDkt7t7WwbTd1RM8iGxu9FexPZcn7C", "amount": "19000025.40000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQShonrAdJXg4CzhegtynzaBP7S631vKGq", "amount": "16577946.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMFJGxGLCbegHL3NZ7HRo3UYaCXhweJVKS", "amount": "14977498.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QcAVj3i3drVkiC9P8FtDgVdYz8Zeu61uNg", "amount": "13142764.62938834", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTDB2mmxEU7L4oUnmBh9F3qhCxfVpJUzaA", "amount": "12960806.75574690", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLgewDr48AZxWbDbRZ12xEvCePwPj9bMBE", "amount": "12684967.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPEoMF2dA7NHrHhsSG9zczCFwx9wFdWvzT", "amount": "10033147.61257500", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQSiZAgdMjadXZRVTHa252AAY2yjZfQKR1", "amount": "10000000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWLhUT55hZHgdD7ayEqwHnF9h62dHV45G1", "amount": "9999999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QjnHejJPyuyTRs7huzcqXtbM6sufWyLf9o", "amount": "9999999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QjJjjBUJSZAMuYiwTyfJTFthH6SrofjG6d", "amount": "8871800.22712502", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRh9zNPmz5McxeWx3iCy2xnBRrBoYDZfMJ", "amount": "8739995.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWGwGBzZG8UeNXJYZq9tjzN6ceriqTgMRd", "amount": "7825760.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRj4VNEthakckhYpCJMEBhEFk12pa7GPJT", "amount": "7810001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QbdHzyw6q2hMAhg12JFbXSEcbrj43xNnYG", "amount": "7593814.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNFWWhmsun3HVUChjck8XqhsQW2tckNxxM", "amount": "7239123.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QeosmemBtE7aosv2QZhq1PTjqpKurKXKzy", "amount": "6185646.80750010", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QU8SzA12SVKPgqpTzRna5itKktGhvEdmgP", "amount": "5931486.64530000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQM9B3ipQcSvATsxb4vgVuYs9kuxgnuBwG", "amount": "5032347.69657146", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLu6X6CVhr6tovNR6k9pQijxN4DHiuLeJS", "amount": "4918400.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQ44LmLWzkWWe2UJfdvsySkd4ZRs1is3sX", "amount": "4384750.69674618", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNK7x1kgiA45Zvc5jZFaoDeDzrBrxivwYd", "amount": "4135907.02294526", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPdkHcN8DBLuxaFf4Q4zXNn3rq2G78VgNt", "amount": "4128493.31116798", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMNdPz11XubtvxXLGeiG3PHKaQW67LkZMp", "amount": "4056950.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QhUWSWWFt6vDy2qNFn68JPTPLjyDrzrh4D", "amount": "3220564.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQ7wKkXyQtkwrgG6zeSHNrnDhXQ6fGUsYS", "amount": "3088411.29750001", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QcxXT317FQi3mq9hrQggw7xZujPX63XCq7", "amount": "2779141.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMTcDA7v1dbRXHkryn4ppiM9Q7LGZFNkVT", "amount": "2752011.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTekAqCoXPy5R7NE78GXxjSaXBuekZge88", "amount": "2740816.93000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVQErjTVBkPTphbyDhPvhHKGX2etNXZsgy", "amount": "1999999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNPfWoyuvYaScXuCdiCM2PEzvD7PWybBvh", "amount": "1749402.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QaGhALQm9h3G6m728ZM9L2rrE7Y63nk9e8", "amount": "1637480.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QV96D15Y3qM1r55odiDf7j2TTYY8jCMJPs", "amount": "1572208.70562500", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLsEy8syg8EXttzajE8AWGg3CxccNR6YML", "amount": "1300001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLeNqYjzid2oJFL2iVtcHeAkyunuTsHae4", "amount": "1185185.44326193", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QaUhQiyBZpZWSmFo56zRu2XXfZVL4D2xii", "amount": "1033451.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMrECKgkohx6ZXEMdLzikqBmAkdyHeQDqL", "amount": "997498.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QR1GoYcR3iasGNWtJjeNcZFPf9BH4TjCeC", "amount": "858153.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNsdyTKN7MVsTYqSyZNPatzRTgNyWDqPZW", "amount": "832163.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMK8v74dCqJ3d3p4JMZZknvQ1wEJovHEjd", "amount": "805444.62885757", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMFysMKXLubP21BCYeYiSNP3v5mLhp81Cd", "amount": "676881.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMdfkckSTcCMKnRRve51SEfZQPVLZg9S7b", "amount": "333086.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QN2UFyKfUsJBHmM5hZrrD2wrwZ1SctSfwQ", "amount": "296438.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMrSxECG64fp2JaoXkw9DYEp6jFPYmh5Jr", "amount": "124684.50000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQRUVwMGifreiLv3FuPTTcA1WbsvhH7Mux", "amount": "102825.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMQAG28pyv2aZVjWbKoRn39Ytir6rLPnTK", "amount": "100745.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWY9HkkCvbKJaqWMEkLbbAJcncywJgy31Q", "amount": "100004.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QV6rVM4Ywh7TzdN1hgSWVrArKxyEUMuMfs", "amount": "100002.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTBjfXU6ctuRFBTGkcJySZLqB3L9BdxNbb", "amount": "100002.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTmKWUXtuQ3ihfigtuLSeFddskmSy3fGN5", "amount": "100002.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQDpKmaKa4uz61CSGVD2zSFaQzeRjaQ8FT", "amount": "100002.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRQvk1PCt7pNm6JSohtEQQdMEu7BrWjmtr", "amount": "100002.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVdEoLEamBDg8MC8uoMe6fsWrpbd9J3mTL", "amount": "100001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QW7o4Cd6tvFLe2WryK4eNmFCxz7a9Bs5pJ", "amount": "100001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWCTesfyodV4CnKF5cP1RCnksEonZhyJEj", "amount": "100001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWCEJXDjfZLUfgCJ5V6Uy16KkdigLqEvvo", "amount": "100001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QV1FfiEGhYRfNSN5HeXL6ZB1t2NeQJ761N", "amount": "100000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVjAVUkKjFHX9N2UQntuPWqHe3PGdBgKgx", "amount": "100000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVEuVYqdLfkAyTU5paKkSj2kGid5X7upPi", "amount": "100000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQEBW8LU48cuV4uiwQUJ94k3UkJo5eszeg", "amount": "100000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUqDXJzwKrte2YNpR4b44ttKWwnSk6pRTZ", "amount": "100000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QgkNZyrj4zAEAwCDZHTLQ7RcbcCTwcNzPL", "amount": "99999.09400001", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QW4jfDfsVWHLeLLvdapCG23q5MGajh8mKr", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVvq5dE352hhujmDcoj6CaEajp7mBuAuKK", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QU6Ac4sVimeehkuK8XcbpFUBVatZ2qgd47", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQodoJ4PdvwWbPwLErJsXjxtrW2BDZ52Gd", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLm1JSR6mph8vXrJ4ZHj5PjchAP7UoN9zm", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QU54p4EUyEr6EDEK7tmWEgwMBkr5MHNJr9", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMAsEHJcNAk6Vkzik2bEV6nJ7JZjWAksvi", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTAth8ahCmt7ZchLA2N6S5jbF5wCMzyC8H", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQ5jfYBm2tPH4xCdfWDccnzUxhNPHL3VS3", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVfFNmHCgWFHhUwLBNBYp32JjL2uusyfsW", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVfru93ZBLd5XR9YzRBbFHCuYiNV1hJhEw", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWA1V7vrRLuhibXjRCSG54y4mkTLf3a4T7", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVxFXg9QMJp7FFD4XPU2BXR4hySCB9u9Zm", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRMpfSZ3gcX4gZKCXHam36qpaCC49F2cEt", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLecTtNrkgGFAwsSPVpJLgkqdjmjLmsT3F", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUVkDZ2ercosZrRBnC8YZt7et9sXfwfKP4", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWR8npVGu3w75p5WtgxMikcnujVHEuPBxL", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMHjTaHRi1zTRgXq1vddRiuMBxZpHEJFeb", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLpBVjVAxhygwbvHyGLLnKbyvsZ1jUqaWC", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVXchvzB4PaaXSwhyZcayiTTSe5cje3DE8", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRWVqUTDFQRRAKiWjkFCk5pdE1iDw2Cz8s", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVKtHgZutURJdqnMCRRewsi2WsFNmYGoEE", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQAiKxdyRuKT7vPTQGbTitiN4E1WVnWZFa", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QSu3hoF3n9qjMSBfmEn6bGVKNXpn4SrtiX", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQsuUfQg57meYToLCx7GTH2DYfAfe4KgQx", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRPAESx3b4pWw9C4Q6YLdNYUcCQsWbzEfE", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRPnVABwiqT3qBqC6bscfWU6R79RjhUfp9", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QR3mxAS6xdwsRGQXiMVngYmL953gdKcaoa", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRSeGbHVKqhbUGNzF33DhoJQiPpf9WxZWR", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRdmMSmXk65qRYD8r1yzMBSKFdZf89nmZ8", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QSYyv5yJBhkrGeB33NV3NbRYTys9mfkzze", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQMFd2LfNJCyH6jkQkrK7mdKpjWiJhhFty", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QP9tgb3pN6FzU3UCmHGW95s9Fmgubzm8md", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQszEg4sqt2o5oN4pdyjxrAwGD3TY5QNfZ", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNLsfHsFcS5xc7BR1fD6aNSZLPt8CEKLuG", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVuKDbWVeM92yEta1fagk79NGQsLkvKdCf", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QSCdg6rRRm4jb3r4XNKVytyQAgiTxkAjTw", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMdEATyg41BFbWuMZx829uzBZ8fqYDDERw", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QM66cjqmF3WCbbiac6tVU1pLupv2AvU4ev", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVeQRaWcPUNfT3yPWtm6xmuy1hM4N6b1yg", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPbWwDxBK3m1LU7uPv55Rq8bwvqLsSjXiE", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QR2cz8ErrLtcos58iKrQA9aPa8T8kaGc5w", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTrMWWXU6WyuBRW3WU6duNXpe7AbNog4kP", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWHinsRV49Nf2ue2ZBumK8jWHWrSFhHr91", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQa6YP9ocmaMBZPEAPnY7dDekdqo9os4gr", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QSqcH6aiqnGnP8iGG1cRxMSqtypsqgWprm", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMrodSbGSU3zsJVi8BN5X5gPE2xvXGt1ja", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPJbzQTJ7T8fRFu5NbGu9goLZyeLq5Kg3i", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTHLtnDuCz85Xa2jkPuZKhdvyXh8yZH6NJ", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMMsPuLhww36VQAphKyo29BfhHLeekf8ST", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPNuGywKCewuy66DXj7GAJrev5HkcVWjuu", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMTN4ALHmKeJkbVFQQyceEAideDTb1eXdq", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRMbQS8Tz9tEH5Wxg6GcoqJ22NqVEcVLFY", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QW1NHhKUUPGg2WwMxNyV8VbzmEJJfBTXvy", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQginD86frBSnoNjHEk7JE4zaBWbTqnHk6", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPSEbEbncD9aUrSzNie658DuoFgex641Sd", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLzhvyT3LHzE9DqFrSwNivKMzkB6T1g2DD", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNG1SXqh2uJ4FAy2yxEzUXbRh9Z5KsTvq5", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QM1KcVDtpkfsrSyDx6v9okdigbXrNTDKmZ", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLnyXdhGDVEBmByTtjTZihVE3UnReghNyd", "amount": "78296.75000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTHQ9pFMoraWTCS1tuFzuwWSXu7uVJ18VF", "amount": "40395.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QfNkfy7kZ2gwue4Vsoz3YfARwSCS13cwXN", "amount": "14997.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QN9QJxXAT2ceDwgXvSvjkndeUGXW84xfhQ", "amount": "8505.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVZAQrQHPD8rePZ5hWKx4uj7Ty4GEBe15D", "amount": "2989.50000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPZTxWtCmH6Y6zwwntjnPDfKG6zNKRivqJ", "amount": "1282.61375000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qgwuvmkqf1wTAxqyFpvUswDqgzWrC6towa", "amount": "0.66600000", "assetId": 1 }
|
||||
{ "type": "GENESIS", "recipient": "QYgVi26jUqMzJo4ahZV9yekQNnYKHBaX8r", "amount": "637557960.49687541", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWe1iPDudLU189BggPykbH1DrAeaFEgX6W", "amount": "400671036.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVurebcEbe4USR4xcS3Mbk12mhxsjRX31u", "amount": "352652351.30705076", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QfhJM5CpX5MhfjCcopwfw7pgS6w1hVJ49E", "amount": "263574650.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QeSh3t1AnaRcRThkkUTvvdMEouixCADeVh", "amount": "232356835.62157121", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QM9zVbXXnfrtQ1X7zPQ5zxPYPAWTaVMXqZ", "amount": "160672815.43629771", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZkThgFfExognAbxLjYZGVHpL7X6g3EG4A", "amount": "128506517.58266672", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVSqUrNFR4mPTMa7UdVmNKZTSaDVAv8XXF", "amount": "126998534.24906898", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXKmtkHHwaUQzGeHHG2dFiHUnKAp815Mzq", "amount": "122690068.33700012", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qft1ktvJ14eBFjpJaphT24ks4WRcN3K6tB", "amount": "114361793.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QcrnYL6yNwHKuEzYLXQ8LewG3m2B5k9K5f", "amount": "100000078.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qe9VPzQp3h4Kg3DHSHBUQ3AM3AiRBfCDfX", "amount": "100000031.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPYfRd1uhnAgqkZNmjNCjgPhkguMnHWuc4", "amount": "100000026.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QREtYDhP4HkpeCCZroemuGXMGVFoZHH3Lp", "amount": "99999999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QaUciVnbQDXdNygJadEY31PuDEBLi6Spmu", "amount": "99008835.47860426", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QjrC8NXwR8gFkEauvRwCPxqHroPFqAJbhK", "amount": "80000207.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QiBYApdEYRwsFYjt59UJqZV55wcwykvhsh", "amount": "62663714.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTTrv8SWR8huV8TFYUEQhfZ1j1JmtL5p8G", "amount": "60036435.28601510", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QaPKuyyQtXJcsVhKLKgxCcYewxwaawxLrB", "amount": "49450000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QbVGj7U4C2tQfipTss4tSzC22bbrF6fNnL", "amount": "46488501.53473695", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qd1Xw41BzN1CgASqsh2PcrrkTKyDs2MVYF", "amount": "44766868.70000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdXe21sjY8smjVmiAUgZY8xWVzwgxMgK5A", "amount": "42447489.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYsh2NB6TogqV1iXHmHXcVaWw25WEYA94o", "amount": "40976709.97984710", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZiYh4m4Uh3FH52cnow8MrNyXhSH88bp2H", "amount": "38172492.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWomBbcXNTdkyuPFUafwtBfpbxHzmUZzqi", "amount": "37291276.42323117", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QisSQZ7Et7Rfzx2SCC2o9UDSeRZWMyFKWc", "amount": "33333332.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QP8xG56L8b28h1mguSk9LuzNhxbHgAoL9b", "amount": "31465829.00562504", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QfmM8dgfikTB2FYVuJ9owzQXVm8wP7T4QT", "amount": "26593505.67231102", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWvTdm9LU1GSX9q6Rrvgx7xjo2iuV2Gxn1", "amount": "25303386.66951814", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYhvqnVFapMA3s74Mu3D1DwPkEkcy1oKPn", "amount": "22476856.86000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUzUCfoakDqBaL5zBgfvTKLHcuxbUfB38Q", "amount": "21967101.30582773", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QgJDnJF1zfoFBxB1LRvi5Rxb8p345UmVos", "amount": "20555555.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRBVE5gHumH6RUgEUxJdQ5417NUvc14k3F", "amount": "20442665.63310002", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QbH8srDKeS5VcsQsgsaF3nqCzGT1NqfsTx", "amount": "20000006.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMuWNAJ2tbeViHtBUN3yD2KARrrzcanLAd", "amount": "19927901.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QSBJuNoCAFUTcevuCTcMi3i5nzNPhC5R4b", "amount": "19000025.40000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdGbhtkFHUqd9nK9UegxxGXD1eSRYSoKjt", "amount": "16577946.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QakZtV3nZeRp5UEoEUNX7p8Qz5VbVXTQid", "amount": "16000000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZvHW7amu5DNktsBgaMrR1brHZhhhVwKLW", "amount": "14977498.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUNYcKorTAjcFEFH2kLuGzTHDSXHbTm9n4", "amount": "13142764.62938834", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXUWuZ2oAUodMU8EAQkAkDwkQHS1SFxpts", "amount": "12960806.75574690", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZw7tgMttSySNMKfcMrEbdtnqHVrQ9w9fT", "amount": "12684967.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXoW5M113C3SDUeFdjVVs2m1RT9XtzXS3z", "amount": "11583150.66666671", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdepeWWLMD3LiDRKkKBria29rDhZP9V5RJ", "amount": "10051008.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qi74UrbLEsCD9D98rRQrsszNtRA4pWbXaN", "amount": "10033147.61257500", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QawB5MesBratjs2d9EMnXnrN4EC7gw7LRw", "amount": "10000000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYpYshfVzMMVU71KqAV4erX3NJAUCnJXgD", "amount": "10000000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QeaDGU85fpffwsw9ngmd98QsT6NaFyFFed", "amount": "9999999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qeq85FoJpxtzoDM93WiNQQCXiuiFynRQzm", "amount": "9999999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QbwgBD6LWdk1hZsb8EwdVVmDZdvpxMzyGT", "amount": "8871800.22712502", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QaFN7DWcGF7keNACJpwCVnegePbfsAoFCw", "amount": "8739995.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QfjL32jLsxtumbfx6ufmfCFCBccVCQFkrh", "amount": "7825760.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNMtHRjEWPgZUVCpiC2qE2LyJsxq1BC1Yj", "amount": "7810001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdtAQm1EGNgM7QDSaC2qvV9WdpRHwpApUT", "amount": "7593814.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QbEJ5tfeskASVnmLFeKzspuZJJb6cJVPZ4", "amount": "7239123.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZfctgpdTkoCcLzpeWSSq7nYQwCLPsoFsL", "amount": "6185646.80750010", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZsygC1chppsJK1cnaHG4fEsNaDYfLF7ZJ", "amount": "5931486.64530000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXmYaDzKQdGiAMncJCr1FqXy6tX3avMRm9", "amount": "5580161.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QgpAW6uqwNR58gNYWRCVXNLm4F5TuckAw4", "amount": "5389659.68181819", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLqeoXwo7RMJUsTv2sKbBffjDvbSSQN7vH", "amount": "5032347.69657146", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QhjovpBRoW2BJ1vRf1xmBcEqENYzexryM6", "amount": "4996995.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQNigUeJxK9WMJoGhydKpfa5y2mQp6bX7v", "amount": "4918400.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMfWg9oJg49izXMeRWrsErgNnBD6mJcKiX", "amount": "4418544.42625000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZjCgcSVvSRsFZeLJz9C5dTa36s3cSKqvB", "amount": "4384750.69674618", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXeiJa7ftN6dAYNZcdqKDBhmAFyGjX2zkm", "amount": "4135907.02294526", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QLsQg11ipfS4ZaD3QTFwncEgCBPsr1r2hj", "amount": "4128493.31116798", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QY7gQwZKiPYMHYjYASSDBhhAoyYaxmex17", "amount": "4056950.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QfPcwetW3BErP4ySTurxFJSHpNkNXPEhGk", "amount": "3220564.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUJhiF8h6ubsXhyATKHccLwD9cz1ECDpaD", "amount": "3088411.29750001", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPtJ6prBJ1NR5NbtuEj7kMaM1a9FALueHA", "amount": "2779141.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QhAkKZZF1ZuC9ADgpfZ8BMZRVV43BiQhuZ", "amount": "2752011.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZxcntxfJimHus9pgfvVVPbpHs5yU7ZMhB", "amount": "2740816.93000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXHEZ4axuNq91K5wW9zaNSvtLzsdsQ1yVz", "amount": "1999999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPhNJXXc97CRBP4JMu2cgiumyfKciBHHyu", "amount": "1749402.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZu4Ka4XHKgTJMc6Y8Kr9cjF12sMKQaPQP", "amount": "1637480.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVEfMEeEHP768c9rEyi3WcH2JQwqheVDe3", "amount": "1572208.70562500", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QhdoF3Kt3dV5DkuPgTmvH3RNzgZrSK9o6W", "amount": "1300001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQ5qnof5pUgJem8NPsAPgYdENL88cNqSj9", "amount": "1185185.44326193", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdceL6F94xZCUmXgrGPKCrsvtPtKmFprDp", "amount": "1033451.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qf2KXaB68Ca6E3uYty14DrEVXGJ6RMg8Tw", "amount": "997720.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QiFUUj4GvfHTTuAhFseuoWZm3wYemqxSDn", "amount": "997498.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXjRdpB4gMjixwP7cv5yeyn8RQR9BuDhet", "amount": "858153.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWez1VBfYVJ8KFoZ6MhJDzYVLbn5mr38VT", "amount": "832163.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPE3EGR8LTYSLtWBq52SFte66oud6cxchF", "amount": "805444.62885757", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZh58Nw9KfPHyhTbVa37iuqAsPGDSzJtka", "amount": "676881.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdUkv47jHcpCB5mZvqEn47XZPzBnxfr8V5", "amount": "333086.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVCzURyACQvd5sYkmMbg35YJvtZjsr2uGC", "amount": "296438.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QfVQpcqzK6RVRWESrrdgyVNmM9sPuxjBC5", "amount": "124684.50000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRMSN3DDrfGcBpqAUfYJb5xz5DovCL2Qy1", "amount": "102825.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWKYjxBUt2c6BHm26c4k7U8iF9eUEEAeQy", "amount": "100745.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qbcy4uyMkQF2JXYqGkueDiFNZ4tHjRg8CR", "amount": "100004.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMtm8wVPHGE3qHg2hMaj6SZ78D5eXw3VWZ", "amount": "100002.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPqfuZpmyA6cK6WUFwcGeKH2Te1aegkHBM", "amount": "100002.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNMDKE7XTujNQkuQorcHXw6hL7qRvyaTjr", "amount": "100002.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTpYQqRyMekaEuECziirzy3HvCVofZS1wJ", "amount": "100002.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWL7kZp6Pdd1bhxZ6SXPhVf5g7GParG9CC", "amount": "100002.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QgcF6KgVZ9eDAMHJdSEeAtp91t931VKZMv", "amount": "100001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMLjFiPiPa46YBRoushe6a227kSrXnXyKb", "amount": "100001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qgp16aMcdiS2EUkxCm5NSZgB8DixGK51zT", "amount": "100001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQPYyoE3Bm2vh8Wr5aaBNyirC8dd3BhBGH", "amount": "100001.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRtRELSSASzqiYy2FtNcrePH6TVnqJkv9B", "amount": "100000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWwrtjBL4ah965XPXHYJhymreC9jyryNLZ", "amount": "100000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMJwdufHY9dMoARHCUyGbMPAqUB4BcqGKm", "amount": "100000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qce2Djqrk2WzG1QhMZ3BqFok9HGsz4wtM3", "amount": "100000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QcCBVfL35rxSyQ416L2MBz14FYbNrbeNPx", "amount": "100000.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QaSL21TX9QH3NTBTXbNCysB2zWRk3vkm6z", "amount": "99999.09400001", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUXga5K8nzd9EqYtvEesZWEYuA688h6D3d", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYD3kXchZ86vUyJBXNCVQ4LUvTAd6PUZW3", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QY1RFZTD2ogRohf3UrdT4g1Qo9D122AZDN", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QSGB4Rd2xhd6UmA9LALTQ4f89Tfsz5VajU", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QY6ZGZdi8h5op2VrRXkG1W5Jp3feLwp7ZD", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRKRk5HVADsN1LHygK7q2pA7dWnYKnPpCT", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUZQPWhrxpze32vGiux6wa85kg9iwuhCDx", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUKKwug9PNai3DBggXUXP8Ag7WmR5SVUR4", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QgCQq4cFaGrJhwvKs4XwvccKiLZ8GVMCXR", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZ19JRpSsgvm4z6EjnbhdxJBoUYzDGvP3x", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QjEaMxcBKMsj91ytKe6GdTBJP8Mu1Ru3r4", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QgqM5bKs3tNqKNAnVeaQp4oaYMXCmX6YJr", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYGNMWBmqWgVtMWGHypAsKhDVQw5mrFZww", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qd1Px9vhWuEmF2SbLx3Ez7HhGtifGMa8TJ", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRWEbzH4niUcu9dL3Yq42X4j89aqQk3qWw", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QScBgSw74MquesXmVJxerX3YgyhtShRr4q", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNHdGeFJmPcDdN8prPzPL4bk2dpnJ2ZZFr", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdXdUxnyKGGo7eEfTcx85oEikNe5nYnuwa", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QgVZb632eqF1eLQm9gBGuBtyp9Dyz2FKUK", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXYk68x2tiUrDBv8eq6wd4KtBmLHYiC4zR", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRCtc67FTNKS5zVXM8omw8F55h9DP7herL", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPEbvVBWDG7qgy4smY8nWiie78Vec8qiT9", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdFZk74skMUu4rKMPEmcSVwR87LNDe6o3Y", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRaDef6H2zYfefqLwYGmUg7T6DAqo6DDqc", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPREQjU2defiYdgA33HDiLNGBpxtuebeqE", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWBFK5h61ZxGfqQpEkwwKTcLAo8t9VWe4K", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QjgGeEkyiXa43pyqkXxZbvAChQpVYfUyKz", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QcNmqT8CZ6zSZwuRm5LahRZnuGBJRnPY8o", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QSq8y4ZrSbF55ZddWNcw1ett2LDtjQEvNn", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPusqAVBVFGAAeE7RdospttA18AuyLP7sB", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNiTnonHpXTeUrgNdyYWVDPP4ZdjkLpW72", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNw9xAm9TUerin9QsapCPL9mV6zmoXyJrh", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QbvxC3ENqomXp11833APchdjeyCNd49nLj", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QRWEbcRnLoGccAndtLcGgpeQFH2ZBcMqHo", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTtXS6fZGThRLq4qgkwM4ngBYkLoFyZ3bK", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QgesuKa3zwx8VAseF1oHZAFHMf29k8ergq", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QhQdzLn36SDgrgoMfvdZAkoWtTUHpB3acJ", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qj5ncwncQY4KPYgKHD1eYpXHbR717PeLcJ", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QjrCFCi6dqvka4UELg2SHhM2oWnQWepd1o", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNVKrjEq5bZdiDtgo64m5kz87rTHqCwvCP", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMozpRT9aUunfmPh7EtQ6LPoth2JFJWBXC", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUGo9SErgc6ceB5aBzcSJDNqBkQ9eaCKZS", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QbYTowTHCr9WzfrR6b8uDfJKwL41nG1vyr", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QQaKBSjAt9RK2bqJoSriR77X4ULstGzrFQ", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QZ2gi6BhUNpGmrErgJLFuY1WHy6xK1J7qX", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWinRb65f2g3yBoaZvTrQKQk7CW7vfBgGX", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTyokTJrR4b2y76An3BFUEbqQy5vvg76iN", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QMsKXQAYKmR7dBH4P3kMLiKzYatK3h1CeS", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QibuD4c6gvXgS4iut7q3sXuVb23rgFJq2M", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QfbX8JJupEw5ckNtU4upQgET35oLTr5e6v", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QbxJvwrEHZs7MDE8rbqBwZAZkcywue5F3W", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYGcPZcRhGaY1MsiDr3VtwTXmB9TAbLFSn", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPyx2bNiAnJEjitfeAh8jZXzQVKio2B7Mi", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYuMATkLjz7YB6s4EG1aWCmmmrAPj3W9Ce", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QcU4VhU9ohDXU4k4AUMapgJRYSzEpizjLN", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QVrvy4ac2jBTfxyCKB7MLimqJooTDBApmS", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QawSgZ7i2LLFTKyPxQptk9gN526ihy5yZi", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdwSxr3t4hdGHjQFy6EVGR9yGMipefsTuo", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTGeQqn3XEFdnnCqvifCFXYdKym7SaHzTd", "amount": "99999.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXm5e16Lq6dnYwpZJ8Rn2cME3ziHZfRRnp", "amount": "78296.75000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QeAa7yawpJqQYk7PNisVD89HezskBRecH6", "amount": "49996.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QfjNGq3SQt17Wi3e1Qp3cHJVeHMUHTe3Ar", "amount": "40395.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYogZKQbwwQV2ym46L53RYNGp3WfACCufj", "amount": "37998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdyyAwnpPEXtHmcz6GMpDoJZmP7rR2EwF1", "amount": "35998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QdceyQF75uirtzG9s7ZD2EeTMUAhWsoyFz", "amount": "35998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QSsCvzpJhynqbNceqh2AYiLRHRZnCWcR9b", "amount": "35998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXRzrQyhShKoVUUwzGEMJK8p5RZqps8wa4", "amount": "34998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QSzaKcGZscNAGR476Bi5VBKqv5meNPAZQK", "amount": "23998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPLPWkys8cyZicdacFreT7TT3a2Jg6dzL6", "amount": "22998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QbBfYeGxPXpNkRMF5TrHA7tccjxCBeX5Vg", "amount": "22998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QXKaS5ydZ4VRGrEvMr94FvNzf7onEbxetE", "amount": "22998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPfJ3FU6jnamrxZJDH3ijH5Tze8UoEmtGu", "amount": "21998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QPZdi91axpQ9mkPbJdhGGjssmT5jaaZNX2", "amount": "20998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QhUWEeKX8EAxX2KomuaVmV3NvZrAV4Eznk", "amount": "14997.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QUg3gHTxunZzZMv5j9BmNoGrDzQbC3tNtN", "amount": "13997.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QNryEH4ryCCMNqdpnSG7qdj1Yr1NN4dRgU", "amount": "9998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QhyZ1DUi2vg4JmG6aLHxqsx2TfzhqCs7L6", "amount": "8998.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "Qa9SH9rEKGDvJakQJiiNp4J1P8XnzYRCAn", "amount": "8505.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QYFxjNYARmD7d3VGwXmfcdejyZ4pvUUs13", "amount": "2989.50000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QWZwi5NUzHuJfg5fh9HzjWRpQc1fmMknAh", "amount": "1282.61375000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QTKFuqgfLCn2JxQq7A2WvLjoYGGMWmr9dj", "amount": "997.00000000", "assetId": 1 },
|
||||
{ "type": "GENESIS", "recipient": "QfwU5G7SHXNaGNmK2YvwieGpG2UsoPibB8", "amount": "0.66600000", "assetId": 1 }
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,56 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<localization>
|
||||
<context locale="de">
|
||||
|
||||
<context path="Api">
|
||||
<context path="BlocksResource">
|
||||
<!--<context path="GET signature">
|
||||
<translation key="operation:description" template="returns the block that matches the given signature" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
<context path="GET first">
|
||||
<translation key="operation:description" template="returns the genesis block" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
<context path="GET last">
|
||||
<translation key="operation:description" template="returns the last valid block" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
<context path="GET child:signature">
|
||||
<translation key="operation:description" template="returns the child block of the block that matches the given signature" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
<context path="GET generatingbalance">
|
||||
<translation key="operation:description" template="calculates the generating balance of the block that will follow the last block" />
|
||||
<translation key="success_response:description" template="the generating balance" />
|
||||
</context>
|
||||
<context path="GET generatingbalance:signature">
|
||||
<translation key="operation:description" template="calculates the generating balance of the block that will follow the block that matches the signature" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
<context path="GET time">
|
||||
<translation key="operation:description" template="calculates the time it should take for the network to generate the next block" />
|
||||
<translation key="success_response:description" template="the time" />
|
||||
</context>
|
||||
<context path="GET time:generatingbalance">
|
||||
<translation key="operation:description" template="calculates the time it should take for the network to generate blocks when the current generating balance in the network is the specified generating balance" />
|
||||
<translation key="success_response:description" template="the time" />
|
||||
</context>
|
||||
<context path="GET height">
|
||||
<translation key="operation:description" template="returns the block height of the last block." />
|
||||
<translation key="success_response:description" template="the height" />
|
||||
</context>
|
||||
<context path="GET height:signature">
|
||||
<translation key="operation:description" template="returns the block height of the block that matches the given signature" />
|
||||
<translation key="success_response:description" template="the height" />
|
||||
</context>
|
||||
<context path="GET byheight:height">
|
||||
<translation key="operation:description" template="returns the block whith given height" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>-->
|
||||
</context>
|
||||
</context>
|
||||
|
||||
</context>
|
||||
|
||||
</localization>
|
@@ -1,56 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<localization>
|
||||
<context locale="en">
|
||||
|
||||
<context path="Api">
|
||||
<context path="BlocksResource">
|
||||
<context path="GET signature">
|
||||
<translation key="operation:description" template="returns the block that matches the given signature" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
<context path="GET first">
|
||||
<translation key="operation:description" template="returns the genesis block" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
<context path="GET last">
|
||||
<translation key="operation:description" template="returns the last valid block" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
<context path="GET child:signature">
|
||||
<translation key="operation:description" template="returns the child block of the block that matches the given signature" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
<context path="GET generatingbalance">
|
||||
<translation key="operation:description" template="calculates the generating balance of the block that will follow the last block" />
|
||||
<translation key="success_response:description" template="the generating balance" />
|
||||
</context>
|
||||
<context path="GET generatingbalance:signature">
|
||||
<translation key="operation:description" template="calculates the generating balance of the block that will follow the block that matches the signature" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
<context path="GET time">
|
||||
<translation key="operation:description" template="calculates the time it should take for the network to generate the next block" />
|
||||
<translation key="success_response:description" template="the time" />
|
||||
</context>
|
||||
<context path="GET time:generatingbalance">
|
||||
<translation key="operation:description" template="calculates the time it should take for the network to generate blocks when the current generating balance in the network is the specified generating balance" />
|
||||
<translation key="success_response:description" template="the time" />
|
||||
</context>
|
||||
<context path="GET height">
|
||||
<translation key="operation:description" template="returns the block height of the last block." />
|
||||
<translation key="success_response:description" template="the height" />
|
||||
</context>
|
||||
<context path="GET height:signature">
|
||||
<translation key="operation:description" template="returns the block height of the block that matches the given signature" />
|
||||
<translation key="success_response:description" template="the height" />
|
||||
</context>
|
||||
<context path="GET byheight:height">
|
||||
<translation key="operation:description" template="returns the block whith given height" />
|
||||
<translation key="success_response:description" template="the block" />
|
||||
</context>
|
||||
</context>
|
||||
</context>
|
||||
|
||||
</context>
|
||||
|
||||
</localization>
|
@@ -1,72 +1,14 @@
|
||||
UNKNOWN=Unbekannter Fehler
|
||||
JSON=JSON Nachricht konnte nicht geparsed werden
|
||||
NO_BALANCE=Guthaben ungenügend
|
||||
NOT_YET_RELEASED=Feature wurde noch nicht veröffentlicht
|
||||
INVALID_SIGNATURE=Ungültige Signatur
|
||||
INVALID_ADDRESS=Ungültige Adresse
|
||||
INVALID_SEED=Ungültiger Seed
|
||||
INVALID_AMOUNT=Ungültiger Betrag
|
||||
INVALID_FEE=Ungültige Gebühr
|
||||
INVALID_SENDER=Ungültiger Sender
|
||||
INVALID_RECIPIENT=Ungültiger Empfänger
|
||||
INVALID_NAME_LENGTH=Ungültige Namenslänge
|
||||
INVALID_VALUE_LENGTH=Ungültige Wertlänge
|
||||
INVALID_NAME_OWNER=Ungültiger Namensbesitzer
|
||||
INVALID_BUYER=Ungültiger Käufer
|
||||
INVALID_PUBLIC_KEY=Ungültiger Public Key
|
||||
INVALID_OPTIONS_LENGTH=Ungültige Optionen-Länge
|
||||
INVALID_OPTION_LENGTH=Ungültige Optionslänge
|
||||
INVALID_DATA=Ungültige Daten
|
||||
INVALID_DATA_LENGTH=Ungültige Datenlänge
|
||||
INVALID_UPDATE_VALUE=Ungültiger Update-Wert
|
||||
KEY_ALREADY_EXISTS=Der Schlüssel existiert bereits, Editieren ist deaktiviert
|
||||
KEY_NOT_EXISTS=Der Schlüssel existiert nicht
|
||||
LAST_KEY_IS_DEFAULT_KEY_ERROR=Du kannst den Schlüssel '${key}' nicht löschen, wenn er der einzige ist
|
||||
FEE_LESS_REQUIRED=fee less required
|
||||
WALLET_NOT_IN_SYNC=Das Wallet muss synchronisiert werden
|
||||
INVALID_NETWORK_ADDRESS=Ungültige Netzwerkadresse
|
||||
WALLET_NO_EXISTS=Das Wallet existiert nicht
|
||||
WALLET_ADDRESS_NO_EXISTS=Die Adresse existiert nicht im Wallet
|
||||
WALLET_LOCKED=Das Wallet ist abgeschlossen
|
||||
WALLET_ALREADY_EXISTS=Das Wallet existiert bereits
|
||||
WALLET_API_CALL_FORBIDDEN_BY_USER=Der Benutzer hat den API-Aufruf abgelehnt
|
||||
BLOCK_NO_EXISTS=Der Block existiert nicht
|
||||
TRANSACTION_NO_EXISTS=Die Transaktion existiert nicht
|
||||
PUBLIC_KEY_NOT_FOUND=Public Key wurde nicht gefunden
|
||||
NAME_NO_EXISTS=Der Name existiert nicht
|
||||
NAME_ALREADY_EXISTS=Der Name existiert bereits
|
||||
NAME_ALREADY_FOR_SALE=Der Name steht bereits zum Verkauf
|
||||
NAME_NOT_LOWER_CASE=Der Name muss aus Kleinbuchstaben bestehen
|
||||
NAME_SALE_NO_EXISTS=Namensverkauf existiert nicht
|
||||
BUYER_ALREADY_OWNER=Der Käufer ist bereits Besitzer
|
||||
POLL_NO_EXISTS=Die Abstimmung existiert nicht
|
||||
POLL_ALREADY_EXISTS=Die Abstimmung existiert bereits
|
||||
DUPLICATE_OPTION=Nicht alle Optionen sind eindeutig
|
||||
POLL_OPTION_NO_EXISTS=Die option existiert nicht
|
||||
ALREADY_VOTED_FOR_THAT_OPTION=Bereits für diese Option abgestimmt
|
||||
INVALID_ASSET_ID=Ungültige Asset ID
|
||||
NAME_NOT_REGISTERED=?NAME_NOT_REGISTERED?
|
||||
NAME_FOR_SALE=?NAME_FOR_SALE?
|
||||
NAME_WITH_SPACE=?NAME_WITH_SPACE?
|
||||
INVALID_DESC_LENGTH=Ungültige Beschreibungslänge. Max. Länge ${MAX_LENGTH}
|
||||
EMPTY_CODE=Der Code ist leer
|
||||
DATA_SIZE=Ungültige Datenlänge
|
||||
NULL_PAGES=Ungültige Seiten
|
||||
INVALID_TYPE_LENGTH=Ungültige Typlänge
|
||||
INVALID_TAGS_LENGTH=Ungültige Tag-Länge
|
||||
INVALID_CREATION_BYTES=Fehler in Creation Bytes
|
||||
BODY_EMPTY=invalid body it must not be empty
|
||||
BLOG_DISABLED=Dieser Blog ist deaktiviert
|
||||
NAME_NOT_OWNER=the creator address does not own the author name
|
||||
TX_AMOUNT=the data size is too large - currently only ${BATCH_TX_AMOUNT} arbitrary transactions are allowed at once!
|
||||
BLOG_ENTRY_NO_EXISTS=transaction with this signature contains no entries!
|
||||
BLOG_EMPTY=this blog is empty
|
||||
POSTID_EMPTY=the attribute postid is empty! this is the signature of the post you want to comment
|
||||
POST_NOT_EXISTING=for the given postid no blogpost to comment was found
|
||||
COMMENTING_DISABLED=commenting is for this blog disabled
|
||||
COMMENT_NOT_EXISTING=for the given signature no comment was found
|
||||
INVALID_COMMENT_OWNER=invalid comment owner
|
||||
MESSAGE_FORMAT_NOT_HEX=the Message format is not hex - correct the text or use isTextMessage = true
|
||||
MESSAGE_BLANK=The message attribute is missing or content is blank
|
||||
NO_PUBLIC_KEY=The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to him.
|
||||
MESSAGESIZE_EXCEEDED=Message size exceeded!
|
||||
|
||||
INVALID_ADDRESS = ung\u00FCltige adresse
|
||||
|
||||
INVALID_ASSET_ID = ung\u00FCltige asset ID
|
||||
|
||||
INVALID_DATA = ung\u00FCltige daten
|
||||
|
||||
INVALID_PUBLIC_KEY = ung\u00FCltiger public key
|
||||
|
||||
INVALID_SIGNATURE = ung\u00FCltige signatur
|
||||
|
||||
JSON = JSON nachricht konnte nicht geparsed werden
|
||||
|
||||
PUBLIC_KEY_NOT_FOUND = public key wurde nicht gefunden
|
||||
|
@@ -1,100 +1,57 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# Keys are from api.ApiError enum
|
||||
|
||||
# Common
|
||||
UNKNOWN=unknown error
|
||||
JSON=failed to parse json message
|
||||
NO_BALANCE=not enough balance
|
||||
NOT_YET_RELEASED=that feature is not yet released
|
||||
UNAUTHORIZED=api call unauthorized
|
||||
REPOSITORY_ISSUE=repository error
|
||||
NON_PRODUCTION=This API call is not permitted for production systems
|
||||
ADDRESS_UNKNOWN = account address unknown
|
||||
|
||||
# Validation
|
||||
INVALID_SIGNATURE=invalid signature
|
||||
INVALID_ADDRESS=invalid address
|
||||
INVALID_SEED=invalid seed
|
||||
INVALID_AMOUNT=invalid amount
|
||||
INVALID_FEE=invalid fee
|
||||
INVALID_SENDER=invalid sender
|
||||
INVALID_RECIPIENT=invalid recipient
|
||||
INVALID_NAME_LENGTH=invalid name length
|
||||
INVALID_VALUE_LENGTH=invalid value length
|
||||
INVALID_NAME_OWNER=invalid name owner
|
||||
INVALID_BUYER=invalid buyer
|
||||
INVALID_PUBLIC_KEY=invalid public key
|
||||
INVALID_OPTIONS_LENGTH=invalid options length
|
||||
INVALID_OPTION_LENGTH=invalid option length
|
||||
INVALID_DATA=invalid data
|
||||
INVALID_DATA_LENGTH=invalid data length
|
||||
INVALID_UPDATE_VALUE=invalid update value
|
||||
KEY_ALREADY_EXISTS=key already exists, edit is false
|
||||
KEY_NOT_EXISTS=the key does not exist
|
||||
FEE_LESS_REQUIRED=fee less required
|
||||
WALLET_NOT_IN_SYNC=wallet needs to be synchronized
|
||||
INVALID_NETWORK_ADDRESS=invalid network address
|
||||
ADDRESS_NO_EXISTS=account address does not exist
|
||||
INVALID_CRITERIA=invalid search criteria
|
||||
INVALID_REFERENCE=invalid reference
|
||||
INVALID_PRIVATE_KEY=invalid private key
|
||||
INVALID_HEIGHT=invalid block height
|
||||
|
||||
# Wallet
|
||||
WALLET_NO_EXISTS=wallet does not exist
|
||||
WALLET_ADDRESS_NO_EXISTS=address does not exist in wallet
|
||||
WALLET_LOCKED=wallet is locked
|
||||
WALLET_ALREADY_EXISTS=wallet already exists
|
||||
WALLET_API_CALL_FORBIDDEN_BY_USER=wallet denied api call
|
||||
BLOCKCHAIN_NEEDS_SYNC = blockchain needs to synchronize first
|
||||
|
||||
# Blocks
|
||||
BLOCK_NO_EXISTS=block does not exist
|
||||
BLOCK_UNKNOWN = block unknown
|
||||
|
||||
# Transactions
|
||||
TRANSACTION_NO_EXISTS=transaction does not exist
|
||||
PUBLIC_KEY_NOT_FOUND=public key not found
|
||||
TRANSACTION_INVALID=transaction invalid: %s
|
||||
CANNOT_MINT = account cannot mint
|
||||
|
||||
# Names
|
||||
NAME_NO_EXISTS=name does not exist
|
||||
NAME_ALREADY_EXISTS=name already exists
|
||||
NAME_ALREADY_FOR_SALE=name already for sale
|
||||
NAME_NOT_LOWER_CASE=name must be lower case
|
||||
NAME_SALE_NO_EXISTS=namesale does not exist
|
||||
BUYER_ALREADY_OWNER=buyer is already owner
|
||||
GROUP_UNKNOWN = group unknown
|
||||
|
||||
# Voting
|
||||
POLL_NO_EXISTS=poll does not exist
|
||||
POLL_ALREADY_EXISTS=poll already exists
|
||||
DUPLICATE_OPTION=not all options are unique
|
||||
POLL_OPTION_NO_EXISTS=option does not exist
|
||||
ALREADY_VOTED_FOR_THAT_OPTION=already voted for that option
|
||||
INVALID_ADDRESS = invalid address
|
||||
|
||||
# Assets
|
||||
INVALID_ASSET_ID=invalid asset id
|
||||
INVALID_ORDER_ID=invalid asset order id
|
||||
ORDER_NO_EXISTS=unknown asset order id
|
||||
INVALID_ASSET_ID = invalid asset id
|
||||
|
||||
# ATs
|
||||
EMPTY_CODE=code is empty
|
||||
DATA_SIZE=invalid data length
|
||||
NULL_PAGES=invalid pages
|
||||
INVALID_TYPE_LENGTH=invalid type length
|
||||
INVALID_TAGS_LENGTH=invalid tags length
|
||||
INVALID_CREATION_BYTES=error in creation bytes
|
||||
INVALID_CRITERIA = invalid search criteria
|
||||
|
||||
# Blogs/Name-storage
|
||||
BODY_EMPTY=invalid body it must not be empty
|
||||
BLOG_DISABLED=this blog is disabled
|
||||
NAME_NOT_OWNER=the creator address does not own the author name
|
||||
BLOG_ENTRY_NO_EXISTS=transaction with this signature contains no entries!
|
||||
BLOG_EMPTY=this blog is empty
|
||||
POSTID_EMPTY=the attribute postid is empty! this is the signature of the post you want to comment
|
||||
POST_NOT_EXISTING=for the given postid no blogpost to comment was found
|
||||
COMMENTING_DISABLED=commenting is for this blog disabled
|
||||
COMMENT_NOT_EXISTING=for the given signature no comment was found
|
||||
INVALID_COMMENT_OWNER=invalid comment owner
|
||||
INVALID_DATA = invalid data
|
||||
|
||||
# Messages
|
||||
MESSAGE_FORMAT_NOT_HEX=the Message format is not hex - correct the text or use isTextMessage = true
|
||||
MESSAGE_BLANK=The message attribute is missing or content is blank
|
||||
NO_PUBLIC_KEY=The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to them.
|
||||
MESSAGESIZE_EXCEEDED=Message size exceeded!
|
||||
INVALID_HEIGHT = invalid block height
|
||||
|
||||
INVALID_NETWORK_ADDRESS = invalid network address
|
||||
|
||||
INVALID_ORDER_ID = invalid asset order ID
|
||||
|
||||
INVALID_PRIVATE_KEY = invalid private key
|
||||
|
||||
INVALID_PUBLIC_KEY = invalid public key
|
||||
|
||||
INVALID_REFERENCE = invalid reference
|
||||
|
||||
# Validation
|
||||
INVALID_SIGNATURE = invalid signature
|
||||
|
||||
JSON = failed to parse json message
|
||||
|
||||
NAME_UNKNOWN = name unknown
|
||||
|
||||
NON_PRODUCTION = this API call is not permitted for production systems
|
||||
|
||||
ORDER_UNKNOWN = unknown asset order ID
|
||||
|
||||
PUBLIC_KEY_NOT_FOUND = public key not found
|
||||
|
||||
REPOSITORY_ISSUE = repository error
|
||||
|
||||
TRANSACTION_INVALID = transaction invalid: %s (%s)
|
||||
|
||||
TRANSACTION_UNKNOWN = transaction unknown
|
||||
|
||||
TRANSFORMATION_ERROR = could not transform JSON into transaction
|
||||
|
||||
UNAUTHORIZED = API call unauthorized
|
||||
|
@@ -1,100 +0,0 @@
|
||||
# Keys are from api.ApiError enum
|
||||
|
||||
# Common
|
||||
UNKNOWN=unknown error
|
||||
JSON=failed to parse json message
|
||||
NO_BALANCE=not enough balance
|
||||
NOT_YET_RELEASED=that feature is not yet released
|
||||
UNAUTHORIZED=api call unauthorized
|
||||
REPOSITORY_ISSUE=repository error
|
||||
NON_PRODUCTION=This API call is not permitted for production systems
|
||||
|
||||
# Validation
|
||||
INVALID_SIGNATURE=invalid signature
|
||||
INVALID_ADDRESS=invalid address
|
||||
INVALID_SEED=invalid seed
|
||||
INVALID_AMOUNT=invalid amount
|
||||
INVALID_FEE=invalid fee
|
||||
INVALID_SENDER=invalid sender
|
||||
INVALID_RECIPIENT=invalid recipient
|
||||
INVALID_NAME_LENGTH=invalid name length
|
||||
INVALID_VALUE_LENGTH=invalid value length
|
||||
INVALID_NAME_OWNER=invalid name owner
|
||||
INVALID_BUYER=invalid buyer
|
||||
INVALID_PUBLIC_KEY=invalid public key
|
||||
INVALID_OPTIONS_LENGTH=invalid options length
|
||||
INVALID_OPTION_LENGTH=invalid option length
|
||||
INVALID_DATA=invalid data
|
||||
INVALID_DATA_LENGTH=invalid data length
|
||||
INVALID_UPDATE_VALUE=invalid update value
|
||||
KEY_ALREADY_EXISTS=key already exists, edit is false
|
||||
KEY_NOT_EXISTS=the key does not exist
|
||||
FEE_LESS_REQUIRED=fee less required
|
||||
WALLET_NOT_IN_SYNC=wallet needs to be synchronized
|
||||
INVALID_NETWORK_ADDRESS=invalid network address
|
||||
ADDRESS_NO_EXISTS=account address does not exist
|
||||
INVALID_CRITERIA=invalid search criteria
|
||||
INVALID_REFERENCE=invalid reference
|
||||
INVALID_PRIVATE_KEY=invalid private key
|
||||
INVALID_HEIGHT=invalid block height
|
||||
|
||||
# Wallet
|
||||
WALLET_NO_EXISTS=wallet does not exist
|
||||
WALLET_ADDRESS_NO_EXISTS=address does not exist in wallet
|
||||
WALLET_LOCKED=wallet is locked
|
||||
WALLET_ALREADY_EXISTS=wallet already exists
|
||||
WALLET_API_CALL_FORBIDDEN_BY_USER=wallet denied api call
|
||||
|
||||
# Blocks
|
||||
BLOCK_NO_EXISTS=block does not exist
|
||||
|
||||
# Transactions
|
||||
TRANSACTION_NO_EXISTS=transaction does not exist
|
||||
PUBLIC_KEY_NOT_FOUND=public key not found
|
||||
TRANSACTION_INVALID=transaction invalid: %s
|
||||
|
||||
# Names
|
||||
NAME_NO_EXISTS=name does not exist
|
||||
NAME_ALREADY_EXISTS=name already exists
|
||||
NAME_ALREADY_FOR_SALE=name already for sale
|
||||
NAME_NOT_LOWER_CASE=name must be lower case
|
||||
NAME_SALE_NO_EXISTS=namesale does not exist
|
||||
BUYER_ALREADY_OWNER=buyer is already owner
|
||||
|
||||
# Voting
|
||||
POLL_NO_EXISTS=poll does not exist
|
||||
POLL_ALREADY_EXISTS=poll already exists
|
||||
DUPLICATE_OPTION=not all options are unique
|
||||
POLL_OPTION_NO_EXISTS=option does not exist
|
||||
ALREADY_VOTED_FOR_THAT_OPTION=already voted for that option
|
||||
|
||||
# Assets
|
||||
INVALID_ASSET_ID=invalid asset id
|
||||
INVALID_ORDER_ID=invalid asset order id
|
||||
ORDER_NO_EXISTS=unknown asset order id
|
||||
|
||||
# ATs
|
||||
EMPTY_CODE=code is empty
|
||||
DATA_SIZE=invalid data length
|
||||
NULL_PAGES=invalid pages
|
||||
INVALID_TYPE_LENGTH=invalid type length
|
||||
INVALID_TAGS_LENGTH=invalid tags length
|
||||
INVALID_CREATION_BYTES=error in creation bytes
|
||||
|
||||
# Blogs/Name-storage
|
||||
BODY_EMPTY=invalid body it must not be empty
|
||||
BLOG_DISABLED=this blog is disabled
|
||||
NAME_NOT_OWNER=the creator address does not own the author name
|
||||
BLOG_ENTRY_NO_EXISTS=transaction with this signature contains no entries!
|
||||
BLOG_EMPTY=this blog is empty
|
||||
POSTID_EMPTY=the attribute postid is empty! this is the signature of the post you want to comment
|
||||
POST_NOT_EXISTING=for the given postid no blogpost to comment was found
|
||||
COMMENTING_DISABLED=commenting is for this blog disabled
|
||||
COMMENT_NOT_EXISTING=for the given signature no comment was found
|
||||
INVALID_COMMENT_OWNER=invalid comment owner
|
||||
|
||||
# Messages
|
||||
MESSAGE_FORMAT_NOT_HEX=the Message format is not hex - correct the text or use isTextMessage = true
|
||||
MESSAGE_BLANK=The message attribute is missing or content is blank
|
||||
NO_PUBLIC_KEY=The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to them.
|
||||
MESSAGESIZE_EXCEEDED=Message size exceeded!
|
@@ -1,6 +1,10 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# SysTray pop-up menu
|
||||
|
||||
APPLYING_UPDATE_AND_RESTARTING = Applying automatic update and restarting...
|
||||
|
||||
AUTO_UPDATE = Auto Update
|
||||
|
||||
BLOCK_HEIGHT = height
|
||||
|
||||
CHECK_TIME_ACCURACY = Check time accuracy
|
||||
@@ -9,6 +13,10 @@ CONNECTION = connection
|
||||
|
||||
CONNECTIONS = connections
|
||||
|
||||
CREATING_BACKUP_OF_DB_FILES = Creating backup of database files...
|
||||
|
||||
DB_BACKUP = Database Backup
|
||||
|
||||
EXIT = Exit
|
||||
|
||||
MINTING_DISABLED = NOT minting
|
||||
@@ -22,8 +30,10 @@ NTP_NAG_TEXT_UNIX = Install NTP service to get an accurate clock.
|
||||
|
||||
NTP_NAG_TEXT_WINDOWS = Select "Synchronize clock" from menu to fix.
|
||||
|
||||
OPEN_NODE_UI = Open Node UI
|
||||
OPEN_UI = Open UI
|
||||
|
||||
SYNCHRONIZE_CLOCK = Synchronize clock
|
||||
|
||||
SYNCHRONIZING_BLOCKCHAIN = Synchronizing
|
||||
|
||||
SYNCHRONIZING_CLOCK = Synchronizing clock
|
||||
|
@@ -22,8 +22,10 @@ NTP_NAG_TEXT_UNIX = \u5B89\u88C5NTP\u670D\u52A1\u4EE5\u83B7\u5F97\u51C6\u786E\u7
|
||||
|
||||
NTP_NAG_TEXT_WINDOWS = \u4ECE\u83DC\u5355\u4E2D\u9009\u62E9\u201C\u540C\u6B65\u65F6\u949F\u201D\u8FDB\u884C\u4FEE\u590D\u3002
|
||||
|
||||
OPEN_NODE_UI = \u5F00\u542F\u754C\u9762
|
||||
OPEN_UI = \u5F00\u542F\u754C\u9762
|
||||
|
||||
SYNCHRONIZE_CLOCK = \u540C\u6B65\u65F6\u949F
|
||||
|
||||
SYNCHRONIZING_BLOCKCHAIN = \u540C\u6B65\u533A\u5757\u94FE
|
||||
|
||||
SYNCHRONIZING_CLOCK = \u540C\u6B65\u7740\u65F6\u949F
|
||||
|
@@ -1,42 +1,176 @@
|
||||
OK=OK
|
||||
INVALID_ADDRESS=INVALID_ADDRESS
|
||||
NEGATIVE_AMOUNT=NEGATIVE_AMOUNT
|
||||
NEGATIVE_FEE=NEGATIVE_FEE
|
||||
NO_BALANCE=NO_BALANCE
|
||||
INVALID_REFERENCE=INVALID_REFERENCE
|
||||
INVALID_NAME_LENGTH=INVALID_NAME_LENGTH
|
||||
INVALID_VALUE_LENGTH=INVALID_VALUE_LENGTH
|
||||
NAME_ALREADY_REGISTERED=NAME_ALREADY_REGISTERED
|
||||
NAME_DOES_NOT_EXIST=NAME_DOES_NOT_EXIST
|
||||
INVALID_NAME_OWNER=INVALID_NAME_OWNER
|
||||
NAME_ALREADY_FOR_SALE=NAME_ALREADY_FOR_SALE
|
||||
NAME_NOT_FOR_SALE=NAME_NOT_FOR_SALE
|
||||
BUYER_ALREADY_OWNER=BUYER_ALREADY_OWNER
|
||||
INVALID_AMOUNT=INVALID_AMOUNT
|
||||
INVALID_SELLER=INVALID_SELLER
|
||||
NAME_NOT_LOWER_CASE=NAME_NOT_LOWER_CASE
|
||||
INVALID_DESCRIPTION_LENGTH=INVALID_DESCRIPTION_LENGTH
|
||||
INVALID_OPTIONS_COUNT=INVALID_OPTIONS_COUNT
|
||||
INVALID_OPTION_LENGTH=INVALID_OPTION_LENGTH
|
||||
DUPLICATE_OPTION=DUPLICATE_OPTION
|
||||
POLL_ALREADY_EXISTS=POLL_ALREADY_EXISTS
|
||||
POLL_DOES_NOT_EXIST=POLL_DOES_NOT_EXIST
|
||||
POLL_OPTION_DOES_NOT_EXIST=POLL_OPTION_DOES_NOT_EXIST
|
||||
ALREADY_VOTED_FOR_THAT_OPTION=ALREADY_VOTED_FOR_THAT_OPTION
|
||||
INVALID_DATA_LENGTH=INVALID_DATA_LENGTH
|
||||
INVALID_QUANTITY=INVALID_QUANTITY
|
||||
ASSET_DOES_NOT_EXIST=ASSET_DOES_NOT_EXIST
|
||||
INVALID_RETURN=INVALID_RETURN
|
||||
HAVE_EQUALS_WANT=HAVE_EQUALS_WANT
|
||||
ORDER_DOES_NOT_EXIST=ORDER_DOES_NOT_EXIST
|
||||
INVALID_ORDER_CREATOR=INVALID_ORDER_CREATOR
|
||||
INVALID_PAYMENTS_COUNT=INVALID_PAYMENTS_COUNT
|
||||
NEGATIVE_PRICE=NEGATIVE_PRICE
|
||||
INVALID_CREATION_BYTES=INVALID_CREATION_BYTES
|
||||
INVALID_TAGS_LENGTH=INVALID_TAGS_LENGTH
|
||||
INVALID_AT_TYPE_LENGTH=INVALID_AT_TYPE_LENGTH
|
||||
INVALID_AT_TRANSACTION=INVALID_AT_TRANSACTION
|
||||
AT_IS_FINISHED=AT_IS_FINISHED
|
||||
ASSET_DOES_NOT_MATCH_AT=ASSET_DOES_NOT_MATCH_AT
|
||||
ASSET_ALREADY_EXISTS=ASSET_ALREADY_EXISTS
|
||||
NOT_YET_RELEASED=NOT_YET_RELEASED
|
||||
|
||||
ACCOUNT_ALREADY_EXISTS = account already exists
|
||||
|
||||
ACCOUNT_CANNOT_REWARD_SHARE = account cannot reward-share
|
||||
|
||||
ALREADY_GROUP_ADMIN = already group admin
|
||||
|
||||
ALREADY_GROUP_MEMBER = already group member
|
||||
|
||||
ALREADY_VOTED_FOR_THAT_OPTION = already voted for that option
|
||||
|
||||
ASSET_ALREADY_EXISTS = asset already exists
|
||||
|
||||
ASSET_DOES_NOT_EXIST = ASSET_DOES_NOT_EXIST
|
||||
|
||||
ASSET_DOES_NOT_MATCH_AT = ASSET_DOES_NOT_MATCH_AT
|
||||
|
||||
ASSET_NOT_SPENDABLE = ASSET_NOT_SPENDABLE
|
||||
|
||||
AT_ALREADY_EXISTS = AT_ALREADY_EXISTS
|
||||
|
||||
AT_IS_FINISHED = AT_IS_FINISHED
|
||||
|
||||
AT_UNKNOWN = AT_UNKNOWN
|
||||
|
||||
BANNED_FROM_GROUP = BANNED_FROM_GROUP
|
||||
|
||||
BAN_EXISTS = BAN_EXISTS
|
||||
|
||||
BAN_UNKNOWN = BAN_UNKNOWN
|
||||
|
||||
BUYER_ALREADY_OWNER = BUYER_ALREADY_OWNER
|
||||
|
||||
CLOCK_NOT_SYNCED = CLOCK_NOT_SYNCED
|
||||
|
||||
DUPLICATE_OPTION = DUPLICATE_OPTION
|
||||
|
||||
GROUP_ALREADY_EXISTS = GROUP_ALREADY_EXISTS
|
||||
|
||||
GROUP_APPROVAL_DECIDED = GROUP_APPROVAL_DECIDED
|
||||
|
||||
GROUP_APPROVAL_NOT_REQUIRED = GROUP_APPROVAL_NOT_REQUIRED
|
||||
|
||||
GROUP_DOES_NOT_EXIST = GROUP_DOES_NOT_EXIST
|
||||
|
||||
GROUP_ID_MISMATCH = GROUP_ID_MISMATCH
|
||||
|
||||
GROUP_OWNER_CANNOT_LEAVE = GROUP_OWNER_CANNOT_LEAVE
|
||||
|
||||
HAVE_EQUALS_WANT = HAVE_EQUALS_WANT
|
||||
|
||||
INSUFFICIENT_FEE = INSUFFICIENT_FEE
|
||||
|
||||
INVALID_ADDRESS = INVALID_ADDRESS
|
||||
|
||||
INVALID_AMOUNT = INVALID_AMOUNT
|
||||
|
||||
INVALID_ASSET_OWNER = INVALID_ASSET_OWNER
|
||||
|
||||
INVALID_AT_TRANSACTION = INVALID_AT_TRANSACTION
|
||||
|
||||
INVALID_AT_TYPE_LENGTH = INVALID_AT_TYPE_LENGTH
|
||||
|
||||
INVALID_CREATION_BYTES = INVALID_CREATION_BYTES
|
||||
|
||||
INVALID_DATA_LENGTH = INVALID_DATA_LENGTH
|
||||
|
||||
INVALID_DESCRIPTION_LENGTH = INVALID_DESCRIPTION_LENGTH
|
||||
|
||||
INVALID_GROUP_APPROVAL_THRESHOLD = INVALID_GROUP_APPROVAL_THRESHOLD
|
||||
|
||||
INVALID_GROUP_ID = INVALID_GROUP_ID
|
||||
|
||||
INVALID_GROUP_OWNER = INVALID_GROUP_OWNER
|
||||
|
||||
INVALID_LIFETIME = INVALID_LIFETIME
|
||||
|
||||
INVALID_NAME_LENGTH = INVALID_NAME_LENGTH
|
||||
|
||||
INVALID_NAME_OWNER = INVALID_NAME_OWNER
|
||||
|
||||
INVALID_OPTIONS_COUNT = INVALID_OPTIONS_COUNT
|
||||
|
||||
INVALID_OPTION_LENGTH = INVALID_OPTION_LENGTH
|
||||
|
||||
INVALID_ORDER_CREATOR = INVALID_ORDER_CREATOR
|
||||
|
||||
INVALID_PAYMENTS_COUNT = INVALID_PAYMENTS_COUNT
|
||||
|
||||
INVALID_PUBLIC_KEY = INVALID_PUBLIC_KEY
|
||||
|
||||
INVALID_QUANTITY = INVALID_QUANTITY
|
||||
|
||||
INVALID_REFERENCE = INVALID_REFERENCE
|
||||
|
||||
INVALID_RETURN = INVALID_RETURN
|
||||
|
||||
INVALID_REWARD_SHARE_PERCENT = INVALID_REWARD_SHARE_PERCENT
|
||||
|
||||
INVALID_SELLER = INVALID_SELLER
|
||||
|
||||
INVALID_TAGS_LENGTH = INVALID_TAGS_LENGTH
|
||||
|
||||
INVALID_TX_GROUP_ID = INVALID_TX_GROUP_ID
|
||||
|
||||
INVALID_VALUE_LENGTH = INVALID_VALUE_LENGTH
|
||||
|
||||
INVITE_UNKNOWN = INVITE_UNKNOWN
|
||||
|
||||
JOIN_REQUEST_EXISTS = JOIN_REQUEST_EXISTS
|
||||
|
||||
MAXIMUM_REWARD_SHARES = MAXIMUM_REWARD_SHARES
|
||||
|
||||
MISSING_CREATOR = MISSING_CREATOR
|
||||
|
||||
MULTIPLE_NAMES_FORBIDDEN = MULTIPLE_NAMES_FORBIDDEN
|
||||
|
||||
NAME_ALREADY_FOR_SALE = NAME_ALREADY_FOR_SALE
|
||||
|
||||
NAME_ALREADY_REGISTERED = NAME_ALREADY_REGISTERED
|
||||
|
||||
NAME_DOES_NOT_EXIST = NAME_DOES_NOT_EXIST
|
||||
|
||||
NAME_NOT_FOR_SALE = NAME_NOT_FOR_SALE
|
||||
|
||||
NAME_NOT_LOWER_CASE = NAME_NOT_LOWER_CASE
|
||||
|
||||
NEGATIVE_AMOUNT = NEGATIVE_AMOUNT
|
||||
|
||||
NEGATIVE_FEE = NEGATIVE_FEE
|
||||
|
||||
NEGATIVE_PRICE = NEGATIVE_PRICE
|
||||
|
||||
NOT_GROUP_ADMIN = NOT_GROUP_ADMIN
|
||||
|
||||
NOT_GROUP_MEMBER = NOT_GROUP_MEMBER
|
||||
|
||||
NOT_MINTING_ACCOUNT = NOT_MINTING_ACCOUNT
|
||||
|
||||
NOT_YET_RELEASED = NOT_YET_RELEASED
|
||||
|
||||
NO_BALANCE = NO_BALANCE
|
||||
|
||||
NO_BLOCKCHAIN_LOCK = node's blockchain currently busy
|
||||
|
||||
NO_FLAG_PERMISSION = NO_FLAG_PERMISSION
|
||||
|
||||
OK = OK
|
||||
|
||||
ORDER_ALREADY_CLOSED = ORDER_ALREADY_CLOSED
|
||||
|
||||
ORDER_DOES_NOT_EXIST = ORDER_DOES_NOT_EXIST
|
||||
|
||||
POLL_ALREADY_EXISTS = POLL_ALREADY_EXISTS
|
||||
|
||||
POLL_DOES_NOT_EXIST = POLL_DOES_NOT_EXIST
|
||||
|
||||
POLL_OPTION_DOES_NOT_EXIST = POLL_OPTION_DOES_NOT_EXIST
|
||||
|
||||
PUBLIC_KEY_UNKNOWN = PUBLIC_KEY_UNKNOWN
|
||||
|
||||
SELF_SHARE_EXISTS = SELF_SHARE_EXISTS
|
||||
|
||||
TIMESTAMP_TOO_NEW = TIMESTAMP_TOO_NEW
|
||||
|
||||
TIMESTAMP_TOO_OLD = TIMESTAMP_TOO_OLD
|
||||
|
||||
TOO_MANY_UNCONFIRMED = TOO_MANY_UNCONFIRMED
|
||||
|
||||
TRANSACTION_ALREADY_CONFIRMED = TRANSACTION_ALREADY_CONFIRMED
|
||||
|
||||
TRANSACTION_ALREADY_EXISTS = TRANSACTION_ALREADY_EXISTS
|
||||
|
||||
TRANSACTION_UNKNOWN = TRANSACTION_UNKNOWN
|
||||
|
||||
TX_GROUP_ID_MISMATCH = TX_GROUP_ID_MISMATCH
|
||||
|
@@ -1 +0,0 @@
|
||||
Node UI goes here!
|
@@ -3,6 +3,7 @@ package org.qortal.test;
|
||||
import org.junit.Test;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.transaction.CreateAssetOrderTransaction;
|
||||
import org.qortal.transaction.CreatePollTransaction;
|
||||
@@ -22,7 +23,7 @@ public class CompatibilityTests extends Common {
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useSettings("test-settings-v1.json");
|
||||
NTP.testMode();
|
||||
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.qortal.utils.ExecuteProduceConsume;
|
||||
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
|
||||
|
||||
public class EPCTests {
|
||||
|
||||
@@ -60,13 +61,10 @@ public class EPCTests {
|
||||
ScheduledExecutorService statusExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
statusExecutor.scheduleAtFixedRate(() -> {
|
||||
synchronized (testEPC) {
|
||||
final long seconds = (System.currentTimeMillis() - start) / 1000L;
|
||||
System.out.println(String.format("After %d second%s, active threads: %d, greatest thread count: %d, tasks produced: %d, tasks consumed: %d",
|
||||
seconds, (seconds != 1 ? "s" : ""),
|
||||
testEPC.getActiveThreadCount(), testEPC.getGreatestActiveThreadCount(),
|
||||
testEPC.getTasksProduced(), testEPC.getTasksConsumed()));
|
||||
}
|
||||
final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
|
||||
final long seconds = (System.currentTimeMillis() - start) / 1000L;
|
||||
System.out.print(String.format("After %d second%s, ", seconds, (seconds != 1 ? "s" : "")));
|
||||
printSnapshot(snapshot);
|
||||
}, 1L, 1L, TimeUnit.SECONDS);
|
||||
|
||||
// Let it run for a minute
|
||||
@@ -78,10 +76,17 @@ public class EPCTests {
|
||||
final long after = System.currentTimeMillis();
|
||||
|
||||
System.out.println(String.format("Shutdown took %d milliseconds", after - before));
|
||||
System.out.println(String.format("Greatest thread count: %d", testEPC.getGreatestActiveThreadCount()));
|
||||
|
||||
System.out.println(String.format("Tasks produced: %d", testEPC.getTasksProduced()));
|
||||
System.out.println(String.format("Tasks consumed: %d", testEPC.getTasksConsumed()));
|
||||
final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
|
||||
System.out.print("After shutdown, ");
|
||||
printSnapshot(snapshot);
|
||||
}
|
||||
|
||||
private void printSnapshot(final StatsSnapshot snapshot) {
|
||||
System.out.println(String.format("threads: %d active (%d max, %d exhaustion%s), tasks: %d produced / %d consumed",
|
||||
snapshot.activeThreadCount, snapshot.greatestActiveThreadCount,
|
||||
snapshot.spawnFailures, (snapshot.spawnFailures != 1 ? "s": ""),
|
||||
snapshot.tasksProduced, snapshot.tasksConsumed));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -12,6 +12,7 @@ import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Serialization;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
@@ -19,6 +20,9 @@ import io.druid.extendedset.intset.ConciseSet;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
@@ -142,4 +146,30 @@ public class SerializationTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPositiveBigDecimal() throws IOException {
|
||||
BigDecimal amount = new BigDecimal("123.4567").setScale(8);
|
||||
|
||||
byte[] bytes = Serialization.serializeBigDecimal(amount);
|
||||
assertEquals("Serialized BigDecimal should be 8 bytes long", 8, bytes.length);
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
|
||||
BigDecimal newAmount = Serialization.deserializeBigDecimal(byteBuffer);
|
||||
|
||||
assertEqualBigDecimals("Deserialized BigDecimal has incorrect value", amount, newAmount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegativeBigDecimal() throws IOException {
|
||||
BigDecimal amount = new BigDecimal("-1.23").setScale(8);
|
||||
|
||||
byte[] bytes = Serialization.serializeBigDecimal(amount);
|
||||
assertEquals("Serialized BigDecimal should be 8 bytes long", 8, bytes.length);
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
|
||||
BigDecimal newAmount = Serialization.deserializeBigDecimal(byteBuffer);
|
||||
|
||||
assertEqualBigDecimals("Deserialized BigDecimal has incorrect value", amount, newAmount);
|
||||
}
|
||||
|
||||
}
|
@@ -5,6 +5,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockMinter;
|
||||
import org.qortal.data.account.AccountData;
|
||||
@@ -19,11 +20,13 @@ import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TestAccount;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class TransferPrivsTests extends Common {
|
||||
|
||||
@@ -42,6 +45,27 @@ public class TransferPrivsTests extends Common {
|
||||
Common.orphanCheck();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAliceIntoNewAccountTransferPrivs() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TestAccount alice = Common.getTestAccount(repository, "alice");
|
||||
assertTrue(alice.canMint());
|
||||
|
||||
PrivateKeyAccount aliceMintingAccount = Common.getTestAccount(repository, "alice-reward-share");
|
||||
|
||||
byte[] randomPublicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
Random random = new Random();
|
||||
random.nextBytes(randomPublicKey);
|
||||
|
||||
Account randomAccount = new PublicKeyAccount(repository, randomPublicKey);
|
||||
|
||||
combineAccounts(repository, alice, randomAccount, aliceMintingAccount);
|
||||
|
||||
assertFalse(alice.canMint());
|
||||
assertTrue(randomAccount.canMint());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAliceIntoDilbertTransferPrivs() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
@@ -94,7 +94,7 @@ public class AssetsApiTests extends ApiCommon {
|
||||
try {
|
||||
assertNotNull(this.assetsResource.getAssetOrder(FAKE_ORDER_ID_BASE58));
|
||||
} catch (ApiException e) {
|
||||
assertTrue(e.error == ApiError.ORDER_NO_EXISTS.getCode());
|
||||
assertTrue(e.error == ApiError.ORDER_UNKNOWN.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,13 +103,13 @@ public class AssetsApiTests extends ApiCommon {
|
||||
try {
|
||||
assertNotNull(this.assetsResource.getAssetOrderTrades(FAKE_ORDER_ID_BASE58, null, null, null));
|
||||
} catch (ApiException e) {
|
||||
assertTrue(e.error == ApiError.ORDER_NO_EXISTS.getCode());
|
||||
assertTrue(e.error == ApiError.ORDER_UNKNOWN.getCode());
|
||||
}
|
||||
|
||||
try {
|
||||
assertNotNull(this.assetsResource.getAssetOrderTrades(FAKE_ORDER_ID_BASE58, 1, 1, true));
|
||||
} catch (ApiException e) {
|
||||
assertTrue(e.error == ApiError.ORDER_NO_EXISTS.getCode());
|
||||
assertTrue(e.error == ApiError.ORDER_UNKNOWN.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
|
60
src/test/java/org/qortal/test/apps/CheckTranslations.java
Normal file
60
src/test/java/org/qortal/test/apps/CheckTranslations.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package org.qortal.test.apps;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.globalization.Translator;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
|
||||
public class CheckTranslations {
|
||||
|
||||
private static final String[] SUPPORTED_LANGS = new String[] { "en", "de", "zh", "ru" };
|
||||
private static final Set<String> SYSTRAY_KEYS = Set.of("AUTO_UPDATE", "APPLYING_UPDATE_AND_RESTARTING", "BLOCK_HEIGHT",
|
||||
"CHECK_TIME_ACCURACY", "CONNECTION", "CONNECTIONS", "CREATING_BACKUP_OF_DB_FILES", "DB_BACKUP", "EXIT",
|
||||
"MINTING_DISABLED", "MINTING_ENABLED", "NTP_NAG_CAPTION", "NTP_NAG_TEXT_UNIX", "NTP_NAG_TEXT_WINDOWS",
|
||||
"OPEN_UI", "SYNCHRONIZE_CLOCK", "SYNCHRONIZING_BLOCKCHAIN", "SYNCHRONIZING_CLOCK");
|
||||
|
||||
private static String failurePrefix;
|
||||
|
||||
public static void main(String[] args) {
|
||||
for (String lang : SUPPORTED_LANGS) {
|
||||
System.out.println(String.format("\n# Checking '%s' translations", lang));
|
||||
|
||||
Locale.setDefault(Locale.forLanguageTag(lang));
|
||||
failurePrefix = "!!" + lang + ":";
|
||||
|
||||
checkTranslations("TransactionValidity", lang, Arrays.stream(ValidationResult.values()).map(value -> value.name()).collect(Collectors.toSet()));
|
||||
checkTranslations("ApiError", lang, Arrays.stream(ApiError.values()).map(value -> value.name()).collect(Collectors.toSet()));
|
||||
|
||||
checkTranslations("SysTray", lang, SYSTRAY_KEYS);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkTranslations(String className, String lang, Set<String> keys) {
|
||||
System.out.println(String.format("## Checking '%s' translations for %s", lang, className));
|
||||
|
||||
Set<String> allKeys = Translator.INSTANCE.keySet(className, lang);
|
||||
if (allKeys == null) {
|
||||
System.out.println(String.format("NO '%s' translations for %s!", lang, className));
|
||||
allKeys = Collections.emptySet();
|
||||
}
|
||||
|
||||
for (String key : keys) {
|
||||
allKeys.remove(key);
|
||||
|
||||
String translation = Translator.INSTANCE.translate(className, lang, key);
|
||||
|
||||
if (translation.startsWith(failurePrefix))
|
||||
System.out.println(String.format("Missing key '%s' in %s_%s.properties", key, className, lang));
|
||||
}
|
||||
|
||||
// Any leftover keys?
|
||||
for (String key : allKeys)
|
||||
System.out.println(String.format("Extraneous key '%s' in %s_%s.properties", key, className, lang));
|
||||
}
|
||||
|
||||
}
|
@@ -1,200 +1,44 @@
|
||||
package org.qortal.test.apps;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletionService;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorCompletionService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.apache.commons.net.ntp.NTPUDPClient;
|
||||
import org.apache.commons.net.ntp.NtpV3Packet;
|
||||
import org.apache.commons.net.ntp.TimeInfo;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.apache.logging.log4j.core.config.LoggerConfig;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class NTPTests {
|
||||
|
||||
private static final List<String> CC_TLDS = Arrays.asList("oceania", "europe", "cn", "asia", "africa");
|
||||
|
||||
public static void main(String[] args) throws UnknownHostException, IOException, InterruptedException {
|
||||
NTPUDPClient client = new NTPUDPClient();
|
||||
client.setDefaultTimeout(2000);
|
||||
|
||||
class NTPServer {
|
||||
private static final int MIN_POLL = 8;
|
||||
|
||||
public char usage = ' ';
|
||||
public String remote;
|
||||
public String refId;
|
||||
public Integer stratum;
|
||||
public char type = 'u'; // unicast
|
||||
public int poll = MIN_POLL;
|
||||
public byte reach = 0;
|
||||
public Long delay;
|
||||
public Double offset;
|
||||
public Double jitter;
|
||||
|
||||
private Deque<Double> offsets = new LinkedList<>();
|
||||
private double totalSquareOffsets = 0.0;
|
||||
private long nextPoll;
|
||||
private Long lastGood;
|
||||
|
||||
public NTPServer(String remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
public boolean poll(NTPUDPClient client) {
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
if (now < this.nextPoll)
|
||||
return false;
|
||||
|
||||
boolean isUpdated = false;
|
||||
try {
|
||||
TimeInfo timeInfo = client.getTime(InetAddress.getByName(remote));
|
||||
|
||||
timeInfo.computeDetails();
|
||||
NtpV3Packet ntpMessage = timeInfo.getMessage();
|
||||
|
||||
this.refId = ntpMessage.getReferenceIdString();
|
||||
this.stratum = ntpMessage.getStratum();
|
||||
this.poll = Math.max(MIN_POLL, 1 << ntpMessage.getPoll());
|
||||
|
||||
this.delay = timeInfo.getDelay();
|
||||
this.offset = (double) timeInfo.getOffset();
|
||||
|
||||
if (this.offsets.size() == 8) {
|
||||
double oldOffset = this.offsets.removeFirst();
|
||||
this.totalSquareOffsets -= oldOffset * oldOffset;
|
||||
}
|
||||
|
||||
this.offsets.addLast(this.offset);
|
||||
this.totalSquareOffsets += this.offset * this.offset;
|
||||
|
||||
this.jitter = Math.sqrt(this.totalSquareOffsets / this.offsets.size());
|
||||
|
||||
this.reach = (byte) ((this.reach << 1) | 1);
|
||||
this.lastGood = now;
|
||||
|
||||
isUpdated = true;
|
||||
} catch (IOException e) {
|
||||
this.reach <<= 1;
|
||||
}
|
||||
|
||||
this.nextPoll = now + this.poll * 1000;
|
||||
return isUpdated;
|
||||
}
|
||||
|
||||
public Integer getWhen() {
|
||||
if (this.lastGood == null)
|
||||
return null;
|
||||
|
||||
return (int) ((System.currentTimeMillis() - this.lastGood) / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
List<NTPServer> ntpServers = new ArrayList<>();
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
List<String> ntpServers = new ArrayList<>();
|
||||
|
||||
for (String ccTld : CC_TLDS)
|
||||
for (int subpool = 0; subpool <=3; ++subpool)
|
||||
ntpServers.add(new NTPServer(subpool + "." + ccTld + ".pool.ntp.org"));
|
||||
for (int subpool = 0; subpool <= 3; ++subpool)
|
||||
ntpServers.add(new String(subpool + "." + ccTld + ".pool.ntp.org"));
|
||||
|
||||
while (true) {
|
||||
Thread.sleep(1000);
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
NTP.shutdownNow();
|
||||
}));
|
||||
|
||||
CompletionService<Boolean> ecs = new ExecutorCompletionService<Boolean>(Executors.newCachedThreadPool());
|
||||
for (NTPServer server : ntpServers)
|
||||
ecs.submit(() -> server.poll(client));
|
||||
Logger ntpLogger = LogManager.getLogger(NTP.class);
|
||||
LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = loggerContext.getConfiguration();
|
||||
LoggerConfig loggerConfig = config.getLoggerConfig(ntpLogger.getName());
|
||||
|
||||
boolean showReport = false;
|
||||
for (int i = 0; i < ntpServers.size(); ++i)
|
||||
try {
|
||||
showReport = ecs.take().get() || showReport;
|
||||
} catch (ExecutionException e) {
|
||||
// skip
|
||||
}
|
||||
loggerConfig.setLevel(Level.TRACE);
|
||||
loggerContext.updateLoggers(config);
|
||||
|
||||
if (showReport) {
|
||||
double s0 = 0;
|
||||
double s1 = 0;
|
||||
double s2 = 0;
|
||||
NTP.start(ntpServers.toArray(new String[0]));
|
||||
|
||||
for (NTPServer server : ntpServers) {
|
||||
if (server.offset == null) {
|
||||
server.usage = ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
server.usage = '+';
|
||||
double value = server.offset * (double) server.stratum;
|
||||
|
||||
s0 += 1;
|
||||
s1 += value;
|
||||
s2 += value * value;
|
||||
}
|
||||
|
||||
if (s0 < ntpServers.size() / 3 + 1) {
|
||||
System.out.println("Not enough replies to calculate network time");
|
||||
} else {
|
||||
double filterStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
|
||||
double filterMean = s1 / s0;
|
||||
|
||||
// Now only consider offsets within 1 stddev?
|
||||
s0 = 0;
|
||||
s1 = 0;
|
||||
s2 = 0;
|
||||
|
||||
for (NTPServer server : ntpServers) {
|
||||
if (server.offset == null || server.reach == 0)
|
||||
continue;
|
||||
|
||||
if (Math.abs(server.offset * (double)server.stratum - filterMean) > filterStddev)
|
||||
continue;
|
||||
|
||||
server.usage = '*';
|
||||
s0 += 1;
|
||||
s1 += server.offset;
|
||||
s2 += server.offset * server.offset;
|
||||
}
|
||||
|
||||
if (s0 <= 1) {
|
||||
System.out.println(String.format("Not enough values to calculate network time. stddev: %7.4f", filterStddev));
|
||||
} else {
|
||||
double mean = s1 / s0;
|
||||
double newStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
|
||||
System.out.println(String.format("filtering stddev: %7.3f, mean: %7.3f, new stddev: %7.3f, nValues: %.0f / %d", filterStddev, mean, newStddev, s0, ntpServers.size()));
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(String.format("%c%16s %16s %2s %c %4s %4s %3s %7s %7s %7s",
|
||||
' ', "remote", "refid", "st", 't', "when", "poll", "reach", "delay", "offset", "jitter"
|
||||
));
|
||||
|
||||
for (NTPServer server : ntpServers)
|
||||
System.out.println(String.format("%c%16.16s %16.16s %2s %c %4s %4d %3o %7s %7s %7s",
|
||||
server.usage,
|
||||
server.remote,
|
||||
formatNull("%s", server.refId, ""),
|
||||
formatNull("%2d", server.stratum, ""),
|
||||
server.type,
|
||||
formatNull("%4d", server.getWhen(), "-"),
|
||||
server.poll,
|
||||
server.reach,
|
||||
formatNull("%5dms", server.delay, ""),
|
||||
formatNull("% 5.0fms", server.offset, ""),
|
||||
formatNull("%5.2fms", server.jitter, "")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatNull(String format, Object arg, String nullOutput) {
|
||||
return arg != null ? String.format(format, arg) : nullOutput;
|
||||
// Endless sleep
|
||||
Thread.sleep(1000000000L);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import org.junit.Test;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.test.common.AccountUtils;
|
||||
import org.qortal.test.common.AssetUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
@@ -19,7 +20,7 @@ public class OldTradingTests extends Common {
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useSettings("test-settings-old-asset.json");
|
||||
NTP.testMode();
|
||||
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
|
||||
}
|
||||
|
||||
@After
|
||||
|
@@ -116,7 +116,7 @@ public class Common {
|
||||
|
||||
public static void useDefaultSettings() throws DataException {
|
||||
useSettings(testSettingsFilename);
|
||||
NTP.testMode();
|
||||
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
|
||||
}
|
||||
|
||||
public static void resetBlockchain() throws DataException {
|
||||
|
@@ -4,9 +4,11 @@ import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -204,4 +206,55 @@ public class RewardTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
/** Test rewards to founders, one in reward-share, the other is self-share. */
|
||||
@Test
|
||||
public void testFounderRewards() throws DataException {
|
||||
Common.useSettings("test-settings-v2-founder-rewards.json");
|
||||
|
||||
BigDecimal perHundred = BigDecimal.valueOf(100L);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
|
||||
|
||||
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
|
||||
|
||||
// Alice to mint, therefore online
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
mintingAndOnlineAccounts.add(aliceSelfShare);
|
||||
|
||||
// Bob self-share NOT online
|
||||
|
||||
// Chloe self-share and reward-share with Dilbert both online
|
||||
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
|
||||
mintingAndOnlineAccounts.add(chloeSelfShare);
|
||||
PrivateKeyAccount chloeDilbertRewardShare = new PrivateKeyAccount(repository, Base58.decode("HuiyqLipUN1V9p1HZfLhyEwmEA6BTaT2qEfjgkwPViV4"));
|
||||
mintingAndOnlineAccounts.add(chloeDilbertRewardShare);
|
||||
|
||||
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
|
||||
// 3 founders (online or not) so blockReward divided by 3
|
||||
BigDecimal founderCount = BigDecimal.valueOf(3L);
|
||||
BigDecimal perFounderReward = blockReward.divide(founderCount, RoundingMode.DOWN);
|
||||
|
||||
// Alice simple self-share so her reward is perFounderReward
|
||||
AccountUtils.assertBalance(repository, "alice", Asset.QORT, perFounderReward);
|
||||
|
||||
// Bob not online so his reward is simply perFounderReward
|
||||
AccountUtils.assertBalance(repository, "bob", Asset.QORT, perFounderReward);
|
||||
|
||||
// Chloe has two reward-shares, so her reward is divided by 2
|
||||
BigDecimal chloeSharesCount = BigDecimal.valueOf(2L);
|
||||
BigDecimal chloePerShareReward = perFounderReward.divide(chloeSharesCount, RoundingMode.DOWN);
|
||||
// Her self-share gets chloePerShareReward
|
||||
BigDecimal chloeExpectedBalance = chloePerShareReward;
|
||||
// Her reward-share with Dilbert: 25% goes to Dilbert
|
||||
BigDecimal dilbertSharePercent = BigDecimal.valueOf(25L);
|
||||
BigDecimal dilbertExpectedBalance = chloePerShareReward.multiply(dilbertSharePercent).divide(perHundred, RoundingMode.DOWN);
|
||||
// The remaining 75% goes to Chloe
|
||||
BigDecimal rewardShareRemaining = chloePerShareReward.subtract(dilbertExpectedBalance);
|
||||
chloeExpectedBalance = chloeExpectedBalance.add(rewardShareRemaining);
|
||||
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeExpectedBalance);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
63
src/test/resources/test-chain-v2-founder-rewards.json
Normal file
63
src/test/resources/test-chain-v2-founder-rewards.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"isTestChain": true,
|
||||
"blockTimestampMargin": 500,
|
||||
"transactionExpiryPeriod": 86400000,
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
{ "height": 21, "reward": 1 }
|
||||
],
|
||||
"sharesByLevel": [
|
||||
{ "levels": [ 1, 2 ], "share": 0.05 },
|
||||
{ "levels": [ 3, 4 ], "share": 0.10 },
|
||||
{ "levels": [ 5, 6 ], "share": 0.15 },
|
||||
{ "levels": [ 7, 8 ], "share": 0.20 },
|
||||
{ "levels": [ 9, 10 ], "share": 0.25 }
|
||||
],
|
||||
"qoraHoldersShare": 0.20,
|
||||
"qoraPerQortReward": 250,
|
||||
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
],
|
||||
"featureTriggers": {
|
||||
"messageHeight": 0,
|
||||
"atHeight": 0,
|
||||
"assetsTimestamp": 0,
|
||||
"votingTimestamp": 0,
|
||||
"arbitraryTimestamp": 0,
|
||||
"powfixTimestamp": 0,
|
||||
"qortalTimestamp": 0,
|
||||
"newAssetPricingTimestamp": 0,
|
||||
"groupApprovalTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
"timestamp": 0,
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 },
|
||||
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "REWARD_SHARE", "minterPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "rewardSharePublicKey": "CcABzvk26TFEHG7Yok84jxyd4oBtLkx8RJdGFVz2csvp", "sharePercent": 100 },
|
||||
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||
{ "type": "REWARD_SHARE", "minterPublicKey": "7KNBj2MnEb6zq1vvKY1q8G2Voctcc2Z1X4avFyEH2eJC", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "rewardSharePublicKey": "6bnEKqZbsCSWryUQnbBT9Umufdu3CapFvxfAni6afhFb", "sharePercent": 100 },
|
||||
{ "type": "REWARD_SHARE", "minterPublicKey": "7KNBj2MnEb6zq1vvKY1q8G2Voctcc2Z1X4avFyEH2eJC", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "rewardSharePublicKey": "Hebh14YXUdJA66Vq8KyffNXHx3NSDUAZaNH9qbfEvf5M", "sharePercent": 25 }
|
||||
|
||||
]
|
||||
}
|
||||
}
|
6
src/test/resources/test-settings-v2-founder-rewards.json
Normal file
6
src/test/resources/test-settings-v2-founder-rewards.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2-founder-rewards.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"minPeers": 0
|
||||
}
|
92
tools/approve-auto-update.sh
Executable file
92
tools/approve-auto-update.sh
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
printf "Searching for auto-update transactions to approve...\n";
|
||||
|
||||
tx=$( curl --silent --url "http://localhost:12391/arbitrary/search?txGroupId=1&service=1&confirmationStatus=CONFIRMED&limit=1&reverse=true" );
|
||||
if fgrep --silent '"approvalStatus":"PENDING"' <<< "${tx}"; then
|
||||
true
|
||||
else
|
||||
echo "Can't find any pending transactions"
|
||||
exit
|
||||
fi
|
||||
|
||||
sig=$( perl -n -e 'print $1 if m/"signature":"(\w+)"/' <<< "${tx}" )
|
||||
if [ -z "${sig}" ]; then
|
||||
printf "Can't find transaction signature in JSON:\n%s\n" "${tx}"
|
||||
exit
|
||||
fi
|
||||
|
||||
printf "Found transaction %s\n" $sig;
|
||||
|
||||
printf "\nPaste your dev account private key:\n";
|
||||
IFS=
|
||||
read -s privkey
|
||||
printf "\n"
|
||||
|
||||
# Convert to public key
|
||||
pubkey=$( curl --silent --url "http://localhost:12391/utils/publickey" --data @- <<< "${privkey}" );
|
||||
if egrep -v --silent '^\w{44,46}$' <<< "${pubkey}"; then
|
||||
printf "Invalid response from API - was your private key correct?\n%s\n" "${pubkey}"
|
||||
exit
|
||||
fi
|
||||
printf "Your public key: %s\n" ${pubkey}
|
||||
|
||||
# Convert to address
|
||||
address=$( curl --silent --url "http://localhost:12391/addresses/convert/${pubkey}" );
|
||||
printf "Your address: %s\n" ${address}
|
||||
|
||||
# Grab last reference
|
||||
lastref=$( curl --silent --url "http://localhost:12391/addresses/lastreference/{$address}" );
|
||||
printf "Your last reference: %s\n" ${lastref}
|
||||
|
||||
# Build GROUP_APPROVAL transaction
|
||||
timestamp=$( date +%s )000
|
||||
tx_json=$( cat <<TX_END
|
||||
{
|
||||
"timestamp": ${timestamp},
|
||||
"reference": "${lastref}",
|
||||
"fee": 0.001,
|
||||
"txGroupId": 0,
|
||||
"adminPublicKey": "${pubkey}",
|
||||
"pendingSignature": "${sig}",
|
||||
"approval": true
|
||||
}
|
||||
TX_END
|
||||
)
|
||||
|
||||
raw_tx=$( curl --silent --header "Content-Type: application/json" --url "http://localhost:12391/groups/approval" --data @- <<< "${tx_json}" )
|
||||
if egrep -v --silent '^\w{100,}' <<< "${raw_tx}"; then
|
||||
printf "Building GROUP_APPROVAL transaction failed:\n%s\n" "${raw_tx}"
|
||||
exit
|
||||
fi
|
||||
printf "\nRaw approval tx:\n%s\n" ${raw_tx}
|
||||
|
||||
# sign
|
||||
sign_json=$( cat <<SIGN_END
|
||||
{
|
||||
"privateKey": "${privkey}",
|
||||
"transactionBytes": "${raw_tx}"
|
||||
}
|
||||
SIGN_END
|
||||
)
|
||||
signed_tx=$( curl --silent --header "Content-Type: application/json" --url "http://localhost:12391/transactions/sign" --data @- <<< "${sign_json}" )
|
||||
printf "\nSigned tx:\n%s\n" ${signed_tx}
|
||||
if egrep -v --silent '^\w{100,}' <<< "${signed_tx}"; then
|
||||
printf "Signing GROUP_APPROVAL transaction failed:\n%s\n" "${signed_tx}"
|
||||
exit
|
||||
fi
|
||||
|
||||
# ready to publish?
|
||||
plural="s"
|
||||
printf "\n"
|
||||
for ((seconds = 5; seconds > 0; seconds--)); do
|
||||
if [ "${seconds}" = "1" ]; then
|
||||
plural=""
|
||||
fi
|
||||
printf "\rBroadcasting in %d second%s...(CTRL-C) to abort " $seconds $plural
|
||||
sleep 1
|
||||
done
|
||||
|
||||
printf "\rBroadcasting signed GROUP_APPROVAL transaction... \n"
|
||||
result=$( curl --silent --url "http://localhost:12391/transactions/process" --data @- <<< "${signed_tx}" )
|
||||
printf "API response:\n%s\n" "${result}"
|
81
tools/build-auto-update.sh
Executable file
81
tools/build-auto-update.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Check we are within a git repo
|
||||
git_dir=$( git rev-parse --show-toplevel )
|
||||
if [ -z "${git_dir}" ]; then
|
||||
echo "Cannot determine top-level directory for git repo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Change to git top-level
|
||||
cd ${git_dir}
|
||||
|
||||
# Check we are in 'master' branch
|
||||
branch_name=$( git symbolic-ref -q HEAD )
|
||||
branch_name=${branch_name##refs/heads/}
|
||||
echo "Current git branch: ${branch_name}"
|
||||
if [ "${branch_name}" != "master" ]; then
|
||||
echo "Unexpected current branch '${branch_name}' - expecting 'master'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract short-form commit hash
|
||||
short_hash=$( git rev-parse --short HEAD )
|
||||
if [ -z "${short_hash}" ]; then
|
||||
echo "Unable to extract short-form current commit hash"
|
||||
exit 1
|
||||
fi
|
||||
echo "HEAD commit is: ${short_hash}"
|
||||
|
||||
# Check there are no uncommitted changes
|
||||
uncommitted=$( git status --short --untracked-files=no )
|
||||
if [ ! -z "${uncommitted}" ]; then
|
||||
echo "Cannot continue due to uncommitted files:"
|
||||
echo "${uncommitted}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine project name
|
||||
project=$( perl -n -e 'if (m/<artifactId>(\w+)<.artifactId>/) { print $1; exit }' pom.xml $)
|
||||
if [ -z "${project}" ]; then
|
||||
echo "Unable to determine project name from pom.xml?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Actually rebuild JAR
|
||||
echo "Building ${project} JAR..."
|
||||
mvn clean package 1>/tmp/${project}-mvn-build.log 2>&1
|
||||
if [ "$?" != "0" -o ! -r target/${project}*.jar ]; then
|
||||
echo "Maven build failed. For details, see /tmp/${project}-mvn-build.log"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert packaged JAR to XORed update form
|
||||
echo "Building ${project}.update..."
|
||||
java -cp target/${project}*.jar org.qortal.XorUpdate target/${project}*.jar ${project}.update
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "Failed to create XORed auto-update JAR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create auto-update branch from this commit
|
||||
update_branch=auto-update-${short_hash}
|
||||
|
||||
if git show-ref --quiet --verify refs/heads/${update_branch}; then
|
||||
echo "Existing auto-update branch based on this commit (${short_hash}) - deleting..."
|
||||
git branch --delete --force ${update_branch}
|
||||
fi
|
||||
|
||||
echo "Checking out new auto-update branch based on this commit (${short_hash})..."
|
||||
git checkout --orphan ${update_branch}
|
||||
git rm --cached -fr . 1>/dev/null
|
||||
|
||||
git add ${project}.update
|
||||
|
||||
git commit --message "XORed, auto-update JAR based on commit ${short_hash}"
|
||||
git push --set-upstream origin --force-with-lease ${update_branch}
|
||||
|
||||
echo "Changing back to 'master' branch"
|
||||
git checkout --force master
|
136
tools/publish-auto-update.pl
Executable file
136
tools/publish-auto-update.pl
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use POSIX;
|
||||
use Getopt::Std;
|
||||
|
||||
sub usage() {
|
||||
die("usage: $0 [-p api-port] dev-private-key\n");
|
||||
}
|
||||
|
||||
my %opt;
|
||||
getopts('p:', \%opt);
|
||||
|
||||
usage() unless @ARGV == 1;
|
||||
|
||||
my $port = $opt{p} || 12391;
|
||||
my $privkey = shift @ARGV;
|
||||
|
||||
my $git_dir = `git rev-parse --show-toplevel`;
|
||||
die("Cannot determine git top level dir\n") unless $git_dir;
|
||||
|
||||
chomp $git_dir;
|
||||
chdir($git_dir) || die("Can't change directory to $git_dir: $!\n");
|
||||
|
||||
open(POM, '<', 'pom.xml') || die ("Can't open 'pom.xml': $!\n");
|
||||
my $project;
|
||||
while (<POM>) {
|
||||
if (m/<artifactId>(\w+)<.artifactId>/o) {
|
||||
$project = $1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
close(POM);
|
||||
|
||||
# short-form commit hash on 'master' branch
|
||||
my $commit_hash = `git show --no-patch --format=%h`;
|
||||
die("Can't find commit hash\n") if ! defined $commit_hash;
|
||||
chomp $commit_hash;
|
||||
printf "Commit hash on 'master' branch: %s\n", $commit_hash;
|
||||
|
||||
# build timestamp / commit timestamp on 'master' branch
|
||||
my $timestamp = `git show --no-patch --format=%ct`;
|
||||
die("Can't determine commit timestamp\n") if ! defined $timestamp;
|
||||
$timestamp *= 1000; # Convert to milliseconds
|
||||
|
||||
# locate sha256 utility
|
||||
my $SHA256 = `which sha256sum || which sha256`;
|
||||
|
||||
# SHA256 of actual update file
|
||||
my $sha256 = `git show auto-update-${commit_hash}:${project}.update | ${SHA256}`;
|
||||
die("Can't calculate SHA256 of ${project}.update\n") unless $sha256 =~ m/(\S{64})/;
|
||||
chomp $sha256;
|
||||
|
||||
# long-form commit hash of HEAD on auto-update branch
|
||||
my $update_hash = `git rev-parse refs/heads/auto-update-${commit_hash}`;
|
||||
die("Can't find commit hash for HEAD on auto-update-${commit_hash} branch\n") if ! defined $update_hash;
|
||||
chomp $update_hash;
|
||||
|
||||
printf "Build timestamp (ms): %d / 0x%016x\n", $timestamp, $timestamp;
|
||||
printf "Auto-update commit hash: %s\n", $update_hash;
|
||||
printf "SHA256 of ${project}.update: %s\n", $sha256;
|
||||
|
||||
my $tx_type = 10;
|
||||
my $tx_timestamp = time() * 1000;
|
||||
my $tx_group_id = 1;
|
||||
my $service = 1;
|
||||
printf "\nARBITRARY(%d) transaction with timestamp %d, txGroupID %d and service %d\n", $tx_type, $tx_timestamp, $tx_group_id, $service;
|
||||
|
||||
my $data_hex = sprintf "%016x%s%s", $timestamp, $update_hash, $sha256;
|
||||
printf "\nARBITRARY transaction data payload: %s\n", $data_hex;
|
||||
|
||||
my $n_payments = 0;
|
||||
my $is_raw = 1; # RAW_DATA
|
||||
my $data_length = length($data_hex) / 2; # two hex chars per byte
|
||||
my $fee = 0.001 * 1e8;
|
||||
|
||||
die("Something's wrong: data length is not 60 bytes!\n") if $data_length != 60;
|
||||
|
||||
my $pubkey = `curl --silent --url http://localhost:${port}/utils/publickey --data ${privkey}`;
|
||||
die("Can't convert private key to public key:\n$pubkey\n") unless $pubkey =~ m/^\w{44}$/;
|
||||
printf "\nPublic key: %s\n", $pubkey;
|
||||
|
||||
my $pubkey_hex = `curl --silent --url http://localhost:${port}/utils/frombase58 --data ${pubkey}`;
|
||||
die("Can't convert base58 public key to hex:\n$pubkey_hex\n") unless $pubkey_hex =~ m/^[A-Za-z0-9]{64}$/;
|
||||
printf "Public key hex: %s\n", $pubkey_hex;
|
||||
|
||||
my $address = `curl --silent --url http://localhost:${port}/addresses/convert/${pubkey}`;
|
||||
die("Can't convert base58 public key to address:\n$address\n") unless $address =~ m/^\w{34}$/;
|
||||
printf "Address: %s\n", $address;
|
||||
|
||||
my $reference = `curl --silent --url http://localhost:${port}/addresses/lastreference/${address}`;
|
||||
die("Can't fetch last reference for $address:\n$reference\n") unless $reference =~ m/^\w{88}$/;
|
||||
printf "Last reference: %s\n", $reference;
|
||||
|
||||
my $reference_hex = `curl --silent --url http://localhost:${port}/utils/frombase58 --data ${reference}`;
|
||||
die("Can't convert base58 reference to hex:\n$reference_hex\n") unless $reference_hex =~ m/^[A-Za-z0-9]{128}$/;
|
||||
printf "Last reference hex: %s\n", $reference_hex;
|
||||
|
||||
my $raw_tx_hex = sprintf("%08x%016x%08x%s%s%08x%08x%02x%08x%s%016x", $tx_type, $tx_timestamp, $tx_group_id, $reference_hex, $pubkey_hex, $n_payments, $service, $is_raw, $data_length, $data_hex, $fee);
|
||||
printf "\nRaw transaction hex:\n%s\n", $raw_tx_hex;
|
||||
|
||||
my $raw_tx = `curl --silent --url http://localhost:${port}/utils/tobase58/${raw_tx_hex}`;
|
||||
die("Can't convert raw transaction hex to base58:\n$raw_tx\n") unless $raw_tx =~ m/^\w{255,265}$/; # Roughly 255 to 265 base58 chars
|
||||
printf "\nRaw transaction (base58):\n%s\n", $raw_tx;
|
||||
|
||||
my $sign_data = qq|' { "privateKey": "${privkey}", "transactionBytes": "${raw_tx}" } '|;
|
||||
my $signed_tx = `curl --silent -H "accept: text/plain" -H "Content-Type: application/json" --url http://localhost:${port}/transactions/sign --data ${sign_data}`;
|
||||
die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{345,355}$/; # +90ish longer than $raw_tx
|
||||
printf "\nSigned transaction:\n%s\n", $signed_tx;
|
||||
|
||||
# Check we can actually fetch update
|
||||
my $origin = `git remote get-url origin`;
|
||||
die("Unable to get github url for 'origin'?\n") unless $origin && $origin =~ m/:(.*)\.git$/;
|
||||
my $repo = $1;
|
||||
my $update_url = "https://github.com/${repo}/raw/${update_hash}/${project}.update";
|
||||
|
||||
my $fetch_result = `curl --silent -o /dev/null --location --range 0-1 --head --write-out '%{http_code}' --url ${update_url}`;
|
||||
die("\nUnable to fetch update from ${update_url}\n") if $fetch_result ne '200';
|
||||
printf "\nUpdate fetchable from ${update_url}\n";
|
||||
|
||||
# Flush STDOUT after every output
|
||||
$| = 1;
|
||||
print "\n";
|
||||
for (my $delay = 5; $delay > 0; --$delay) {
|
||||
printf "\rSubmitting transaction in %d second%s... CTRL-C to abort ", $delay, ($delay != 1 ? 's' : '');
|
||||
sleep 1;
|
||||
}
|
||||
|
||||
printf "\rSubmitting transaction NOW... \n";
|
||||
my $result = `curl --silent --url http://localhost:${port}/transactions/process --data ${signed_tx}`;
|
||||
chomp $result;
|
||||
die("Transaction wasn't accepted:\n$result\n") unless $result eq 'true';
|
||||
|
||||
my $decoded_tx = `curl --silent -H "Content-Type: application/json" --url http://localhost:${port}/transactions/decode --data ${signed_tx}`;
|
||||
printf "\nTransaction accepted:\n$decoded_tx\n";
|
Reference in New Issue
Block a user