forked from Qortal/qortal
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4e775a107 | ||
|
|
5d6811bd50 | ||
|
|
ecfa6e994e | ||
|
|
ed4a45f214 | ||
|
|
e953be6e4a | ||
|
|
bd51806a0d | ||
|
|
625dbfbbd7 | ||
|
|
fc7a7a1549 | ||
|
|
a12045c19e | ||
|
|
62ae49b639 | ||
|
|
2e8f58bb2f | ||
|
|
9e98ce220f | ||
|
|
10c3a0c056 | ||
|
|
69ec654e4a | ||
|
|
a310e751bb | ||
|
|
3ef8b81e51 | ||
|
|
1f409235e4 | ||
|
|
806baa6ae4 | ||
|
|
58ed72058f | ||
|
|
253a994438 | ||
|
|
5549eded38 | ||
|
|
20777363cf | ||
|
|
b3f859f290 | ||
|
|
8c9f68a9c3 | ||
|
|
41f178bf59 | ||
|
|
ad5050f92e | ||
|
|
16397852ae | ||
|
|
c125a53655 | ||
|
|
7b056a832f | ||
|
|
6c40727027 | ||
|
|
8f06765caf | ||
|
|
de2fc78ad1 | ||
|
|
ee08410260 | ||
|
|
88da8d949f | ||
|
|
d2a92db921 | ||
|
|
9c18a33d7f | ||
|
|
f3b8258067 | ||
|
|
da78c73485 | ||
|
|
cec25ce279 | ||
|
|
0389007491 | ||
|
|
38a64bdd9e | ||
|
|
6a24f787c4 | ||
|
|
98564aa8bf | ||
|
|
9ceff90f42 | ||
|
|
6a4388fecc |
@@ -19,10 +19,10 @@
|
||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||
<ROW Property="NTP_GOOD" Value="false"/>
|
||||
<ROW Property="ProductCode" Value="1033:{3F23DC7A-BC0B-4598-9FD4-C4B927A10D7F} 1049:{CF0D5DDC-7CB7-4308-8F98-DF8D2DB2D38D} 2052:{983B77E5-62CF-431C-B015-B96C5DCA6858} 2057:{BF1C757A-A3A0-4285-906A-6D8D91D74D0A} " Type="16"/>
|
||||
<ROW Property="ProductCode" Value="1033:{F45F964E-1F1F-4B24-AAC5-687C656B5534} 1049:{F228D3BD-A49D-4332-A57D-67A5EFA47674} 2052:{17BB4192-98DA-4D79-AA29-7340ADA1EB38} 2057:{F25873D9-9179-4B35-98FB-EA8D19EE89DE} " Type="16"/>
|
||||
<ROW Property="ProductLanguage" Value="2057"/>
|
||||
<ROW Property="ProductName" Value="Qortal"/>
|
||||
<ROW Property="ProductVersion" Value="1.3.5" Type="32"/>
|
||||
<ROW Property="ProductVersion" Value="1.3.7" Type="32"/>
|
||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
@@ -174,7 +174,7 @@
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_97" ComponentId="{D5544706-E2A7-424F-AEA5-3963E355AA29}" Directory_="jdk.crypto.mscapi_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_97" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_98" ComponentId="{104DBCE8-A458-4B3E-9EFA-2D8613561619}" Directory_="jdk.dynalink_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_98" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_99" ComponentId="{D02E3C37-E81A-48FA-9E28-B26B728AECD9}" Directory_="jdk.httpserver_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_99" Type="0"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{5FCFB67B-FDD0-4B15-9A58-2188092589E9}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{7D827076-2762-468D-BC89-813DBA8A8B89}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
||||
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
||||
<ROW Component="DATA_PATH" ComponentId="{EE0B6107-E244-4CDB-B195-E9038D2F1E0E}" Directory_="DATA_PATH" Attributes="0"/>
|
||||
|
||||
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.3.8</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
||||
@@ -3,13 +3,14 @@
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.3.7</release>
|
||||
<release>1.3.8</release>
|
||||
<versions>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.5</version>
|
||||
<version>1.3.6</version>
|
||||
<version>1.3.7</version>
|
||||
<version>1.3.8</version>
|
||||
</versions>
|
||||
<lastUpdated>20200812131412</lastUpdated>
|
||||
<lastUpdated>20200925114415</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
||||
13
pom.xml
13
pom.xml
@@ -3,13 +3,14 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>1.3.5</version>
|
||||
<version>1.3.8</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
<bitcoinj.version>0.15.5</bitcoinj.version>
|
||||
<bouncycastle.version>1.64</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
<ciyam-at.version>1.3.7</ciyam-at.version>
|
||||
<ciyam-at.version>1.3.8</ciyam-at.version>
|
||||
<commons-net.version>3.6</commons-net.version>
|
||||
<commons-text.version>1.8</commons-text.version>
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
@@ -312,6 +313,14 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.2</version>
|
||||
<configuration>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
|
||||
@@ -5,10 +5,20 @@ import java.net.UnknownHostException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public class Security {
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public abstract class Security {
|
||||
|
||||
public static final String API_KEY_HEADER = "X-API-KEY";
|
||||
|
||||
// TODO: replace with proper authentication
|
||||
public static void checkApiCallAllowed(HttpServletRequest request) {
|
||||
String expectedApiKey = Settings.getInstance().getApiKey();
|
||||
String passedApiKey = request.getHeader(API_KEY_HEADER);
|
||||
|
||||
if ((expectedApiKey != null && !expectedApiKey.equals(passedApiKey)) ||
|
||||
(passedApiKey != null && !passedApiKey.equals(expectedApiKey)))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||
|
||||
InetAddress remoteAddr;
|
||||
try {
|
||||
remoteAddr = InetAddress.getByName(request.getRemoteAddr());
|
||||
@@ -19,4 +29,5 @@ public class Security {
|
||||
if (!remoteAddr.isLoopbackAddress())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -13,17 +14,61 @@ import org.qortal.transaction.Transaction.TransactionType;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ActivitySummary {
|
||||
|
||||
public int blockCount;
|
||||
public int transactionCount;
|
||||
public int assetsIssued;
|
||||
public int namesRegistered;
|
||||
private int blockCount;
|
||||
private int assetsIssued;
|
||||
private int namesRegistered;
|
||||
|
||||
// Assuming TransactionType values are contiguous so 'length' equals count
|
||||
@XmlJavaTypeAdapter(TransactionCountMapXmlAdapter.class)
|
||||
public Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
|
||||
private Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
|
||||
private int totalTransactionCount = 0;
|
||||
|
||||
public ActivitySummary() {
|
||||
// Needed for JAXB
|
||||
}
|
||||
|
||||
public int getBlockCount() {
|
||||
return this.blockCount;
|
||||
}
|
||||
|
||||
public void setBlockCount(int blockCount) {
|
||||
this.blockCount = blockCount;
|
||||
}
|
||||
|
||||
public int getTotalTransactionCount() {
|
||||
return this.totalTransactionCount;
|
||||
}
|
||||
|
||||
public int getAssetsIssued() {
|
||||
return this.assetsIssued;
|
||||
}
|
||||
|
||||
public void setAssetsIssued(int assetsIssued) {
|
||||
this.assetsIssued = assetsIssued;
|
||||
}
|
||||
|
||||
public int getNamesRegistered() {
|
||||
return this.namesRegistered;
|
||||
}
|
||||
|
||||
public void setNamesRegistered(int namesRegistered) {
|
||||
this.namesRegistered = namesRegistered;
|
||||
}
|
||||
|
||||
public Map<TransactionType, Integer> getTransactionCountByType() {
|
||||
return Collections.unmodifiableMap(this.transactionCountByType);
|
||||
}
|
||||
|
||||
public void setTransactionCountByType(TransactionType transactionType, int transactionCount) {
|
||||
this.transactionCountByType.put(transactionType, transactionCount);
|
||||
|
||||
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
|
||||
}
|
||||
|
||||
public void setTransactionCountByType(Map<TransactionType, Integer> transactionCountByType) {
|
||||
this.transactionCountByType = new EnumMap<>(transactionCountByType);
|
||||
|
||||
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BlockInfo {
|
||||
|
||||
private byte[] signature;
|
||||
private int height;
|
||||
private long timestamp;
|
||||
private int transactionCount;
|
||||
private String minterAddress;
|
||||
|
||||
protected BlockInfo() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
public BlockInfo(byte[] signature, int height, long timestamp, int transactionCount, String minterAddress) {
|
||||
this.signature = signature;
|
||||
this.height = height;
|
||||
this.timestamp = timestamp;
|
||||
this.transactionCount = transactionCount;
|
||||
this.minterAddress = minterAddress;
|
||||
}
|
||||
|
||||
public BlockInfo(BlockData blockData) {
|
||||
// Convert BlockData to BlockInfo, using additional data
|
||||
this.minterAddress = "unknown?";
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(blockData.getMinterPublicKey());
|
||||
if (rewardShareData != null)
|
||||
this.minterAddress = rewardShareData.getMintingAccount();
|
||||
} catch (DataException e) {
|
||||
// We'll carry on with placeholder minterAddress then...
|
||||
}
|
||||
|
||||
this.signature = blockData.getSignature();
|
||||
this.height = blockData.getHeight();
|
||||
this.timestamp = blockData.getTimestamp();
|
||||
this.transactionCount = blockData.getTransactionCount();
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public int getTransactionCount() {
|
||||
return this.transactionCount;
|
||||
}
|
||||
|
||||
public String getMinterAddress() {
|
||||
return this.minterAddress;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -473,6 +474,7 @@ public class AddressesResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String computePublicize(String rawBytes58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -133,6 +134,7 @@ public class AdminResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public NodeStatus status() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -153,6 +155,7 @@ public class AdminResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String shutdown() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -181,7 +184,10 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ActivitySummary summary() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
ActivitySummary summary = new ActivitySummary();
|
||||
|
||||
LocalDate date = LocalDate.now();
|
||||
@@ -193,16 +199,13 @@ public class AdminResource {
|
||||
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start);
|
||||
int endHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
summary.blockCount = endHeight - startHeight;
|
||||
summary.setBlockCount(endHeight - startHeight);
|
||||
|
||||
summary.transactionCountByType = repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight);
|
||||
summary.setTransactionCountByType(repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight));
|
||||
|
||||
for (Integer count : summary.transactionCountByType.values())
|
||||
summary.transactionCount += count;
|
||||
summary.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(start).size());
|
||||
|
||||
summary.assetsIssued = repository.getAssetRepository().getRecentAssetIds(start).size();
|
||||
|
||||
summary.namesRegistered = repository.getNameRepository().getRecentNames(start).size();
|
||||
summary.setNamesRegistered (repository.getNameRepository().getRecentNames(start).size());
|
||||
|
||||
return summary;
|
||||
} catch (DataException e) {
|
||||
@@ -210,6 +213,30 @@ public class AdminResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/enginestats")
|
||||
@Operation(
|
||||
summary = "Fetch statistics snapshot for core engine",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = Controller.StatsSnapshot.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public Controller.StatsSnapshot getEngineStats() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
return Controller.getInstance().getStatsSnapshot();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/mintingaccounts")
|
||||
@Operation(
|
||||
@@ -222,6 +249,7 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public List<MintingAccountData> getMintingAccounts() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -268,6 +296,7 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addMintingAccount(String seed58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -320,6 +349,7 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String deleteMintingAccount(String key58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -419,6 +449,7 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String orphan(String targetHeightString) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -460,6 +491,7 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String forceSync(String targetPeerAddress) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -498,6 +530,162 @@ public class AdminResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/repository/data")
|
||||
@Operation(
|
||||
summary = "Export sensitive/node-local data from repository.",
|
||||
description = "Exports data to .script files on local machine"
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String exportRepository() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.exportNodeLocalData();
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform export
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/data")
|
||||
@Operation(
|
||||
summary = "Import data into repository.",
|
||||
description = "Imports data from file on local machine. Filename is forced to 'import.script' if apiKey is not set.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string", example = "MintingAccounts.script"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String importRepository(String filename) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
// Hard-coded because it's too dangerous to allow user-supplied filenames in weaker security contexts
|
||||
if (Settings.getInstance().getApiKey() == null)
|
||||
filename = "import.script";
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.importDataFromFile(filename);
|
||||
repository.saveChanges();
|
||||
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform import
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/checkpoint")
|
||||
@Operation(
|
||||
summary = "Checkpoint data in repository.",
|
||||
description = "Forces repository to checkpoint uncommitted writes.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String checkpointRepository() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.checkpoint(true);
|
||||
repository.saveChanges();
|
||||
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform checkpoint
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/backup")
|
||||
@Operation(
|
||||
summary = "Perform online backup of repository.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String backupRepository() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.backup(true);
|
||||
repository.saveChanges();
|
||||
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform backup
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/repository")
|
||||
@Operation(
|
||||
@@ -505,6 +693,7 @@ public class AdminResource {
|
||||
description = "Requires enough free space to rebuild repository. This will pause your node for a while."
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public void performRepositoryMaintenance() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.extensions.Extension;
|
||||
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.annotations.security.SecuritySchemes;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import org.qortal.api.Security;
|
||||
|
||||
@OpenAPIDefinition(
|
||||
info = @Info( title = "Qortal API", description = "NOTE: byte-arrays are encoded in Base58" ),
|
||||
tags = {
|
||||
@@ -30,5 +36,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
})
|
||||
}
|
||||
)
|
||||
@SecuritySchemes({
|
||||
@SecurityScheme(name = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic"),
|
||||
@SecurityScheme(name = "apiKey", type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, paramName = Security.API_KEY_HEADER)
|
||||
})
|
||||
public class ApiDefinition {
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import javax.ws.rs.core.MediaType;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
@@ -492,7 +491,7 @@ public class BlocksResource {
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = BlockInfo.class
|
||||
implementation = BlockSummaryData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -502,7 +501,7 @@ public class BlocksResource {
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<BlockInfo> getBlockRange(
|
||||
public List<BlockSummaryData> getBlockSummaries(
|
||||
@QueryParam("start") Integer startHeight,
|
||||
@QueryParam("end") Integer endHeight,
|
||||
@Parameter(ref = "count") @QueryParam("count") Integer count) {
|
||||
@@ -515,7 +514,7 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().getBlockInfos(startHeight, endHeight, count);
|
||||
return repository.getBlockRepository().getBlockSummaries(startHeight, endHeight, count);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
@@ -156,6 +157,7 @@ public class ChatResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String buildChat(ChatTransactionData transactionData) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -203,6 +205,7 @@ public class ChatResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String buildChat(String rawBytes58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -155,6 +156,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String buildTrade(CrossChainBuildRequest tradeRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -250,6 +252,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String buildTradeMessage(CrossChainTradeRequest tradeRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -333,6 +336,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String buildRedeemMessage(CrossChainSecretRequest secretRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -404,6 +408,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String buildCancelMessage(CrossChainCancelRequest cancelRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -459,6 +464,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String deriveP2shA(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -485,6 +491,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String deriveP2shB(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -542,6 +549,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public CrossChainBitcoinP2SHStatus checkP2shA(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -568,6 +576,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public CrossChainBitcoinP2SHStatus checkP2shB(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -656,6 +665,7 @@ public class CrossChainResource {
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String refundP2shA(CrossChainBitcoinRefundRequest refundRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -683,6 +693,7 @@ public class CrossChainResource {
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String refundP2shB(CrossChainBitcoinRefundRequest refundRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -793,6 +804,7 @@ public class CrossChainResource {
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String redeemP2shA(CrossChainBitcoinRedeemRequest redeemRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -821,6 +833,7 @@ public class CrossChainResource {
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String redeemP2shB(CrossChainBitcoinRedeemRequest redeemRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -935,6 +948,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String getBitcoinWalletBalance(String xprv58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -969,6 +983,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String sendBitcoin(BitcoinSendRequest bitcoinSendRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -1019,6 +1034,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public List<TradeBotData> getTradeBotStates() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -1049,6 +1065,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -1104,6 +1121,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String tradeBotResponder(TradeBotRespondRequest tradeBotRespondRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -1168,6 +1186,7 @@ public class CrossChainResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String tradeBotDelete(String tradePrivateKey58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
|
||||
@@ -6,8 +6,11 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -26,10 +29,17 @@ import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.ConnectedPeer;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.PeerAddress;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.ExecuteProduceConsume;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@@ -122,6 +132,7 @@ public class PeersResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ExecuteProduceConsume.StatsSnapshot getEngineStats() {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -159,6 +170,7 @@ public class PeersResource {
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addPeer(String address) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -213,6 +225,7 @@ public class PeersResource {
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removePeer(String address) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -248,6 +261,7 @@ public class PeersResource {
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeKnownPeers(String address) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -260,4 +274,68 @@ public class PeersResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/commonblock")
|
||||
@Operation(
|
||||
summary = "Report common block with given peer.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string", example = "node2.qortal.org"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the block",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = BlockSummaryData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public List<BlockSummaryData> commonBlock(String targetPeerAddress) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
// Try to resolve passed address to make things easier
|
||||
PeerAddress peerAddress = PeerAddress.fromString(targetPeerAddress);
|
||||
InetSocketAddress resolvedAddress = peerAddress.toSocketAddress();
|
||||
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
Peer targetPeer = peers.stream().filter(peer -> peer.getResolvedAddress().equals(resolvedAddress)).findFirst().orElse(null);
|
||||
|
||||
if (targetPeer == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int ourInitialHeight = Controller.getInstance().getChainHeight();
|
||||
boolean force = true;
|
||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||
|
||||
SynchronizationResult findCommonBlockResult = Synchronizer.getInstance().fetchSummariesFromCommonBlock(repository, targetPeer, ourInitialHeight, force, peerBlockSummaries);
|
||||
if (findCommonBlockResult != SynchronizationResult.OK)
|
||||
return null;
|
||||
|
||||
return peerBlockSummaries;
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
} catch (UnknownHostException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
} catch (InterruptedException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
@@ -41,10 +41,10 @@ public class BlocksWebSocket extends ApiWebSocket implements Listener {
|
||||
return;
|
||||
|
||||
BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData();
|
||||
BlockInfo blockInfo = new BlockInfo(blockData);
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
|
||||
|
||||
for (Session session : getSessions())
|
||||
sendBlockInfo(session, blockInfo);
|
||||
sendBlockSummary(session, blockSummary);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
@@ -85,13 +85,13 @@ public class BlocksWebSocket extends ApiWebSocket implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
|
||||
if (blockInfos == null || blockInfos.isEmpty()) {
|
||||
List<BlockSummaryData> blockSummaries = repository.getBlockRepository().getBlockSummaries(height, height);
|
||||
if (blockSummaries == null || blockSummaries.isEmpty()) {
|
||||
sendError(session, ApiError.BLOCK_UNKNOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
sendBlockInfo(session, blockInfos.get(0));
|
||||
sendBlockSummary(session, blockSummaries.get(0));
|
||||
} catch (DataException e) {
|
||||
sendError(session, ApiError.REPOSITORY_ISSUE);
|
||||
}
|
||||
@@ -114,23 +114,23 @@ public class BlocksWebSocket extends ApiWebSocket implements Listener {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
|
||||
if (blockInfos == null || blockInfos.isEmpty()) {
|
||||
List<BlockSummaryData> blockSummaries = repository.getBlockRepository().getBlockSummaries(height, height);
|
||||
if (blockSummaries == null || blockSummaries.isEmpty()) {
|
||||
sendError(session, ApiError.BLOCK_UNKNOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
sendBlockInfo(session, blockInfos.get(0));
|
||||
sendBlockSummary(session, blockSummaries.get(0));
|
||||
} catch (DataException e) {
|
||||
sendError(session, ApiError.REPOSITORY_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendBlockInfo(Session session, BlockInfo blockInfo) {
|
||||
private void sendBlockSummary(Session session, BlockSummaryData blockSummary) {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
marshall(stringWriter, blockInfo);
|
||||
marshall(stringWriter, blockSummary);
|
||||
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import static java.util.stream.Collectors.toMap;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -15,6 +17,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.Account;
|
||||
@@ -354,12 +357,8 @@ public class Block {
|
||||
System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
|
||||
}
|
||||
|
||||
byte[] minterSignature;
|
||||
try {
|
||||
minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData.getMinterSignature(), minter, encodedOnlineAccounts));
|
||||
} catch (TransformationException e) {
|
||||
throw new DataException("Unable to calculate next block minter signature", e);
|
||||
}
|
||||
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData.getMinterSignature(),
|
||||
minter.getPublicKey(), encodedOnlineAccounts));
|
||||
|
||||
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
|
||||
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
|
||||
@@ -425,12 +424,8 @@ public class Block {
|
||||
int version = this.blockData.getVersion();
|
||||
byte[] reference = this.blockData.getReference();
|
||||
|
||||
byte[] minterSignature;
|
||||
try {
|
||||
minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData.getMinterSignature(), minter, this.blockData.getEncodedOnlineAccounts()));
|
||||
} catch (TransformationException e) {
|
||||
throw new DataException("Unable to calculate next block's minter signature", e);
|
||||
}
|
||||
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData.getMinterSignature(),
|
||||
minter.getPublicKey(), this.blockData.getEncodedOnlineAccounts()));
|
||||
|
||||
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
|
||||
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
|
||||
@@ -791,15 +786,46 @@ public class Block {
|
||||
return BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(ACCOUNTS_COUNT_SHIFT).add(keyDistance);
|
||||
}
|
||||
|
||||
public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List<BlockSummaryData> blockSummaries) {
|
||||
public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List<BlockSummaryData> blockSummaries, int maxHeight) {
|
||||
BigInteger cumulativeWeight = BigInteger.ZERO;
|
||||
int parentHeight = commonBlockHeight;
|
||||
byte[] parentBlockSignature = commonBlockSignature;
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
boolean isLogging = LOGGER.getLevel().isLessSpecificThan(Level.TRACE);
|
||||
|
||||
for (BlockSummaryData blockSummaryData : blockSummaries) {
|
||||
cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT).add(calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData));
|
||||
StringBuilder stringBuilder = isLogging ? new StringBuilder(512) : null;
|
||||
|
||||
if (isLogging)
|
||||
stringBuilder.append(formatter.format(cumulativeWeight)).append(" -> ");
|
||||
|
||||
cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT);
|
||||
if (isLogging)
|
||||
stringBuilder.append(formatter.format(cumulativeWeight)).append(" + ");
|
||||
|
||||
BigInteger blockWeight = calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
|
||||
if (isLogging)
|
||||
stringBuilder.append("(height: ")
|
||||
.append(parentHeight + 1)
|
||||
.append(", online: ")
|
||||
.append(blockSummaryData.getOnlineAccountsCount())
|
||||
.append(") ")
|
||||
.append(formatter.format(blockWeight));
|
||||
|
||||
cumulativeWeight = cumulativeWeight.add(blockWeight);
|
||||
if (isLogging)
|
||||
stringBuilder.append(" -> ").append(formatter.format(cumulativeWeight));
|
||||
|
||||
if (isLogging && blockSummaries.size() > 1)
|
||||
LOGGER.debug(() -> stringBuilder.toString()); //NOSONAR S1612 (false positive?)
|
||||
|
||||
parentHeight = blockSummaryData.getHeight();
|
||||
parentBlockSignature = blockSummaryData.getSignature();
|
||||
|
||||
/* Potential future consensus change: only comparing the same number of blocks.
|
||||
if (parentHeight >= maxHeight)
|
||||
break;
|
||||
*/
|
||||
}
|
||||
|
||||
return cumulativeWeight;
|
||||
@@ -1061,6 +1087,10 @@ public class Block {
|
||||
// Create repository savepoint here so we can rollback to it after testing transactions
|
||||
repository.setSavepoint();
|
||||
|
||||
if (this.blockData.getHeight() == 212937)
|
||||
// Apply fix for block 212937 but fix will be rolled back before we exit method
|
||||
Block212937.processFix(this);
|
||||
|
||||
for (Transaction transaction : this.getTransactions()) {
|
||||
TransactionData transactionData = transaction.getTransactionData();
|
||||
|
||||
@@ -1264,6 +1294,10 @@ public class Block {
|
||||
|
||||
// Distribute block rewards, including transaction fees, before transactions processed
|
||||
processBlockRewards();
|
||||
|
||||
if (this.blockData.getHeight() == 212937)
|
||||
// Apply fix for block 212937
|
||||
Block212937.processFix(this);
|
||||
}
|
||||
|
||||
// We're about to (test-)process a batch of transactions,
|
||||
@@ -1492,6 +1526,10 @@ public class Block {
|
||||
// Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc.
|
||||
this.cachedExpandedAccounts = null;
|
||||
|
||||
if (this.blockData.getHeight() == 212937)
|
||||
// Revert fix for block 212937
|
||||
Block212937.orphanFix(this);
|
||||
|
||||
// Block rewards, including transaction fees, removed after transactions undone
|
||||
orphanBlockRewards();
|
||||
|
||||
|
||||
153
src/main/java/org/qortal/block/Block212937.java
Normal file
153
src/main/java/org/qortal/block/Block212937.java
Normal file
@@ -0,0 +1,153 @@
|
||||
package org.qortal.block;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.UnmarshalException;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.persistence.jaxb.JAXBContextFactory;
|
||||
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
/**
|
||||
* Block 212937
|
||||
* <p>
|
||||
* Somehow a node minted a version of block 212937 that contained one transaction:
|
||||
* a PAYMENT transaction that attempted to spend more QORT than that account had as QORT balance.
|
||||
* <p>
|
||||
* This invalid transaction made block 212937 (rightly) invalid to several nodes,
|
||||
* which refused to use that block.
|
||||
* However, it seems there were no other nodes minting an alternative, valid block at that time
|
||||
* and so the chain stalled for several nodes in the network.
|
||||
* <p>
|
||||
* Additionally, the invalid block 212937 affected all new installations, regardless of whether
|
||||
* they synchronized from scratch (block 1) or used an 'official release' bootstrap.
|
||||
* <p>
|
||||
* After lengthy diagnosis, it was discovered that
|
||||
* the invalid transaction seemed to rely on incorrect balances in a corrupted database.
|
||||
* Copies of DB files containing the broken chain were also shared around, exacerbating the problem.
|
||||
* <p>
|
||||
* There were three options:
|
||||
* <ol>
|
||||
* <li>roll back the chain to last known valid block 212936 and re-mint empty blocks to current height</li>
|
||||
* <li>keep existing chain, but apply database edits at block 212937 to allow current chain to be valid</li>
|
||||
* <li>attempt to mint an alternative chain, retaining as many valid transactions as possible</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* Option 1 was highly undesirable due to knock-on effects from wiping 700+ transactions, some of which
|
||||
* might have affect cross-chain trades, although there were no cross-chain trade completed during
|
||||
* the decision period.
|
||||
* <p>
|
||||
* Option 3 was essentially a slightly better version of option 1 and rejected for similar reasons.
|
||||
* Attempts at option 3 also rapidly hit cumulative problems with every replacement block due to
|
||||
* differing block timestamps making some transactions, and then even some blocks themselves, invalid.
|
||||
* <p>
|
||||
* This class is the implementation of option 2.
|
||||
* <p>
|
||||
* The change in account balances are relatively small, see <tt>block-212937-deltas.json</tt> resource
|
||||
* for actual values. These values were obtained by exporting the <tt>AccountBalances</tt> table from
|
||||
* both versions of the database with chain at block 212936, and then comparing. The values were also
|
||||
* tested by syncing both databases up to block 225500, re-exporting and re-comparing.
|
||||
* <p>
|
||||
* The invalid block 212937 signature is: <tt>2J3GVJjv...qavh6KkQ</tt>.
|
||||
* <p>
|
||||
* The invalid transaction in block 212937 is:
|
||||
* <p>
|
||||
* <code><pre>
|
||||
{
|
||||
"amount" : "0.10788294",
|
||||
"approvalStatus" : "NOT_REQUIRED",
|
||||
"blockHeight" : 212937,
|
||||
"creatorAddress" : "QLdw5uabviLJgRGkRiydAFmAtZzxHfNXSs",
|
||||
"fee" : "0.00100000",
|
||||
"recipient" : "QZi1mNHDbiLvsytxTgxDr9nhJe4pNZaWpw",
|
||||
"reference" : "J6JukdTVuXZ3JYbHatfZzwxG2vSiZwVCPDzW5K7PsVQKRj8XZeDtqnkGCGGjaSQZ9bQMtV44ky88NnGM4YBQKU6",
|
||||
"senderPublicKey" : "DBFfbD2M3uh4jPE5PaUcZVvNPfrrJzVB7seeEtBn5SPs",
|
||||
"signature" : "qkitxdCEEnKt8w6wRfFixtErbXsxWE6zG2ESNhpqBdScikV1WxeA6WZTTMJVV4tCeZdBFXw3V1X5NVztv6LirWK",
|
||||
"timestamp" : 1607863074904,
|
||||
"txGroupId" : 0,
|
||||
"type" : "PAYMENT"
|
||||
}
|
||||
</pre></code>
|
||||
* <p>
|
||||
* Account <tt>QLdw5uabviLJgRGkRiydAFmAtZzxHfNXSs</tt> attempted to spend <tt>0.10888294</tt> (including fees)
|
||||
* when their QORT balance was really only <tt>0.10886665</tt>.
|
||||
* <p>
|
||||
* However, on the broken DB nodes, their balance
|
||||
* seemed to be <tt>0.10890293</tt> which was sufficient to make the transaction valid.
|
||||
*/
|
||||
public final class Block212937 {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(Block212937.class);
|
||||
private static final String ACCOUNT_DELTAS_SOURCE = "block-212937-deltas.json";
|
||||
|
||||
private static final List<AccountBalanceData> accountDeltas = readAccountDeltas();
|
||||
|
||||
private Block212937() {
|
||||
/* Do not instantiate */
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<AccountBalanceData> readAccountDeltas() {
|
||||
Unmarshaller unmarshaller;
|
||||
|
||||
try {
|
||||
// Create JAXB context aware of classes we need to unmarshal
|
||||
JAXBContext jc = JAXBContextFactory.createContext(new Class[] {
|
||||
AccountBalanceData.class
|
||||
}, null);
|
||||
|
||||
// Create unmarshaller
|
||||
unmarshaller = jc.createUnmarshaller();
|
||||
|
||||
// Set the unmarshaller media type to JSON
|
||||
unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, "application/json");
|
||||
|
||||
// Tell unmarshaller that there's no JSON root element in the JSON input
|
||||
unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false);
|
||||
} catch (JAXBException e) {
|
||||
String message = "Failed to setup unmarshaller to read block 212937 deltas";
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
|
||||
ClassLoader classLoader = BlockChain.class.getClassLoader();
|
||||
InputStream in = classLoader.getResourceAsStream(ACCOUNT_DELTAS_SOURCE);
|
||||
StreamSource jsonSource = new StreamSource(in);
|
||||
|
||||
try {
|
||||
// Attempt to unmarshal JSON stream to BlockChain config
|
||||
return (List<AccountBalanceData>) unmarshaller.unmarshal(jsonSource, AccountBalanceData.class).getValue();
|
||||
} catch (UnmarshalException e) {
|
||||
String message = "Failed to parse block 212937 deltas";
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
} catch (JAXBException e) {
|
||||
String message = "Unexpected JAXB issue while processing block 212937 deltas";
|
||||
LOGGER.error(message, e);
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void processFix(Block block) throws DataException {
|
||||
block.repository.getAccountRepository().modifyAssetBalances(accountDeltas);
|
||||
}
|
||||
|
||||
public static void orphanFix(Block block) throws DataException {
|
||||
// Create inverse deltas
|
||||
List<AccountBalanceData> inverseDeltas = accountDeltas.stream()
|
||||
.map(delta -> new AccountBalanceData(delta.getAddress(), delta.getAssetId(), 0 - delta.getBalance()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
block.repository.getAccountRepository().modifyAssetBalances(inverseDeltas);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -531,7 +531,8 @@ public class BlockChain {
|
||||
|
||||
private static void rebuildBlockchain() throws DataException {
|
||||
// (Re)build repository
|
||||
RepositoryManager.rebuild();
|
||||
if (!RepositoryManager.wasPristineAtOpen())
|
||||
RepositoryManager.rebuild();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
GenesisBlock genesisBlock = GenesisBlock.getInstance(repository);
|
||||
@@ -568,7 +569,7 @@ public class BlockChain {
|
||||
orphanBlockData = repository.getBlockRepository().fromHeight(height);
|
||||
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onNewBlock(orphanBlockData);
|
||||
Controller.getInstance().onOrphanedBlock(orphanBlockData);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -18,6 +18,8 @@ public class AtStatesTrimmer implements Runnable {
|
||||
Thread.currentThread().setName("AT States trimmer");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.saveChanges();
|
||||
|
||||
@@ -30,6 +32,10 @@ public class AtStatesTrimmer implements Runnable {
|
||||
if (chainTip == null || NTP.getTime() == null)
|
||||
continue;
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing())
|
||||
continue;
|
||||
|
||||
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
|
||||
// We want to keep AT states near the tip of our copy of blockchain so we can process/orphan nearby blocks
|
||||
long chainTrimmableTimestamp = chainTip.getTimestamp() - Settings.getInstance().getAtStatesMaxLifetime();
|
||||
@@ -37,8 +43,6 @@ public class AtStatesTrimmer implements Runnable {
|
||||
long upperTrimmableTimestamp = Math.min(currentTrimmableTimestamp, chainTrimmableTimestamp);
|
||||
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
|
||||
|
||||
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
|
||||
int upperBatchHeight = trimStartHeight + Settings.getInstance().getAtStatesTrimBatchSize();
|
||||
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
|
||||
|
||||
@@ -49,17 +53,20 @@ public class AtStatesTrimmer implements Runnable {
|
||||
repository.saveChanges();
|
||||
|
||||
if (numAtStatesTrimmed > 0) {
|
||||
final int finalTrimStartHeight = trimStartHeight;
|
||||
LOGGER.debug(() -> String.format("Trimmed %d AT state%s between blocks %d and %d",
|
||||
numAtStatesTrimmed, (numAtStatesTrimmed != 1 ? "s" : ""),
|
||||
trimStartHeight, upperTrimHeight));
|
||||
finalTrimStartHeight, upperTrimHeight));
|
||||
} else {
|
||||
// Can we move onto next batch?
|
||||
if (upperTrimmableHeight > upperBatchHeight) {
|
||||
repository.getATRepository().setAtTrimHeight(upperBatchHeight);
|
||||
trimStartHeight = upperBatchHeight;
|
||||
repository.getATRepository().setAtTrimHeight(trimStartHeight);
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.debug(() -> String.format("Bumping AT state trim height to %d", upperBatchHeight));
|
||||
final int finalTrimStartHeight = trimStartHeight;
|
||||
LOGGER.debug(() -> String.format("Bumping AT state base trim height to %d", finalTrimStartHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
@@ -23,10 +25,15 @@ import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
@@ -135,9 +142,21 @@ public class Controller extends Thread {
|
||||
private ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
|
||||
private volatile boolean notifyGroupMembershipChange = false;
|
||||
|
||||
private volatile BlockData chainTip = null;
|
||||
private static final int BLOCK_CACHE_SIZE = 10; // To cover typical Synchronizer request + a few spare
|
||||
/** Latest blocks on our chain. Note: tail/last is the latest block. */
|
||||
private final Deque<BlockData> latestBlocks = new LinkedList<>();
|
||||
|
||||
/** Cache of BlockMessages, indexed by block signature */
|
||||
@SuppressWarnings("serial")
|
||||
private final LinkedHashMap<ByteArray, BlockMessage> blockMessageCache = new LinkedHashMap<>() {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<ByteArray, BlockMessage> eldest) {
|
||||
return this.size() > BLOCK_CACHE_SIZE;
|
||||
}
|
||||
};
|
||||
|
||||
private long repositoryBackupTimestamp = startTime; // ms
|
||||
private long repositoryCheckpointTimestamp = startTime; // ms
|
||||
private long ntpCheckTimestamp = startTime; // ms
|
||||
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
|
||||
|
||||
@@ -183,6 +202,47 @@ public class Controller extends Thread {
|
||||
/** Cache of latest blocks' online accounts */
|
||||
Deque<List<OnlineAccountData>> latestBlocksOnlineAccounts = new ArrayDeque<>(MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS);
|
||||
|
||||
// Stats
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class StatsSnapshot {
|
||||
public static class GetBlockMessageStats {
|
||||
public AtomicLong requests = new AtomicLong();
|
||||
public AtomicLong cacheHits = new AtomicLong();
|
||||
public AtomicLong unknownBlocks = new AtomicLong();
|
||||
public AtomicLong cacheFills = new AtomicLong();
|
||||
|
||||
public GetBlockMessageStats() {
|
||||
}
|
||||
}
|
||||
public GetBlockMessageStats getBlockMessageStats = new GetBlockMessageStats();
|
||||
|
||||
public static class GetBlockSummariesStats {
|
||||
public AtomicLong requests = new AtomicLong();
|
||||
public AtomicLong cacheHits = new AtomicLong();
|
||||
public AtomicLong fullyFromCache = new AtomicLong();
|
||||
|
||||
public GetBlockSummariesStats() {
|
||||
}
|
||||
}
|
||||
public GetBlockSummariesStats getBlockSummariesStats = new GetBlockSummariesStats();
|
||||
|
||||
public static class GetBlockSignaturesV2Stats {
|
||||
public AtomicLong requests = new AtomicLong();
|
||||
public AtomicLong cacheHits = new AtomicLong();
|
||||
public AtomicLong fullyFromCache = new AtomicLong();
|
||||
|
||||
public GetBlockSignaturesV2Stats() {
|
||||
}
|
||||
}
|
||||
public GetBlockSignaturesV2Stats getBlockSignaturesV2Stats = new GetBlockSignaturesV2Stats();
|
||||
|
||||
public AtomicLong latestBlocksCacheRefills = new AtomicLong();
|
||||
|
||||
public StatsSnapshot() {
|
||||
}
|
||||
}
|
||||
private final StatsSnapshot stats = new StatsSnapshot();
|
||||
|
||||
// Constructors
|
||||
|
||||
private Controller(String[] args) {
|
||||
@@ -238,21 +298,36 @@ public class Controller extends Thread {
|
||||
|
||||
/** Returns current blockchain height, or 0 if it's not available. */
|
||||
public int getChainHeight() {
|
||||
BlockData blockData = this.chainTip;
|
||||
if (blockData == null)
|
||||
return 0;
|
||||
synchronized (this.latestBlocks) {
|
||||
BlockData blockData = this.latestBlocks.peekLast();
|
||||
if (blockData == null)
|
||||
return 0;
|
||||
|
||||
return blockData.getHeight();
|
||||
return blockData.getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns highest block, or null if it's not available. */
|
||||
public BlockData getChainTip() {
|
||||
return this.chainTip;
|
||||
synchronized (this.latestBlocks) {
|
||||
return this.latestBlocks.peekLast();
|
||||
}
|
||||
}
|
||||
|
||||
/** Cache new blockchain tip. */
|
||||
public void setChainTip(BlockData blockData) {
|
||||
this.chainTip = blockData;
|
||||
public void refillLatestBlocksCache() throws DataException {
|
||||
// Set initial chain height/tip
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
synchronized (this.latestBlocks) {
|
||||
this.latestBlocks.clear();
|
||||
|
||||
for (int i = 0; i < BLOCK_CACHE_SIZE && blockData != null; ++i) {
|
||||
this.latestBlocks.addFirst(blockData);
|
||||
blockData = repository.getBlockRepository().fromHeight(blockData.getHeight() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ReentrantLock getBlockchainLock() {
|
||||
@@ -334,13 +409,8 @@ public class Controller extends Thread {
|
||||
try {
|
||||
BlockChain.validate();
|
||||
|
||||
// Set initial chain height/tip
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
Controller.getInstance().setChainTip(blockData);
|
||||
LOGGER.info(String.format("Our chain height at start-up: %d", blockData.getHeight()));
|
||||
}
|
||||
Controller.getInstance().refillLatestBlocksCache();
|
||||
LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight()));
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Couldn't validate blockchain", e);
|
||||
Gui.getInstance().fatalError("Blockchain validation issue", e);
|
||||
@@ -415,6 +485,7 @@ public class Controller extends Thread {
|
||||
Thread.currentThread().setName("Controller");
|
||||
|
||||
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
|
||||
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
|
||||
|
||||
ExecutorService trimExecutor = Executors.newCachedThreadPool(new DaemonThreadFactory());
|
||||
trimExecutor.execute(new AtStatesTrimmer());
|
||||
@@ -460,6 +531,18 @@ public class Controller extends Thread {
|
||||
final long requestMinimumTimestamp = now - ARBITRARY_REQUEST_TIMEOUT;
|
||||
arbitraryDataRequests.entrySet().removeIf(entry -> entry.getValue().getC() < requestMinimumTimestamp);
|
||||
|
||||
// Time to 'checkpoint' uncommitted repository writes?
|
||||
if (now >= repositoryCheckpointTimestamp + repositoryCheckpointInterval) {
|
||||
repositoryCheckpointTimestamp = now + repositoryCheckpointInterval;
|
||||
|
||||
if (Settings.getInstance().getShowCheckpointNotification())
|
||||
SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "DB_CHECKPOINT"),
|
||||
Translator.INSTANCE.translate("SysTray", "PERFORMING_DB_CHECKPOINT"),
|
||||
MessageType.INFO);
|
||||
|
||||
RepositoryManager.checkpoint(true);
|
||||
}
|
||||
|
||||
// Give repository a chance to backup (if enabled)
|
||||
if (repositoryBackupInterval > 0 && now >= repositoryBackupTimestamp + repositoryBackupInterval) {
|
||||
repositoryBackupTimestamp = now + repositoryBackupInterval;
|
||||
@@ -572,9 +655,10 @@ public class Controller extends Thread {
|
||||
|
||||
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
||||
boolean hasStatusChanged = false;
|
||||
BlockData priorChainTip = this.getChainTip();
|
||||
|
||||
synchronized (this.syncLock) {
|
||||
this.syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||
this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||
|
||||
// Only update SysTray if we're potentially changing height
|
||||
if (this.syncPercent < 100) {
|
||||
@@ -586,8 +670,6 @@ public class Controller extends Thread {
|
||||
if (hasStatusChanged)
|
||||
updateSysTray();
|
||||
|
||||
BlockData priorChainTip = this.chainTip;
|
||||
|
||||
try {
|
||||
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
|
||||
switch (syncResult) {
|
||||
@@ -656,9 +738,6 @@ public class Controller extends Thread {
|
||||
// Reset our cache of inferior chains
|
||||
inferiorChainSignatures.clear();
|
||||
|
||||
// Update chain-tip, systray, notify peers, websockets, etc.
|
||||
this.onNewBlock(newChainTip);
|
||||
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
|
||||
}
|
||||
@@ -853,11 +932,99 @@ public class Controller extends Thread {
|
||||
// Protective copy
|
||||
BlockData blockDataCopy = new BlockData(latestBlockData);
|
||||
|
||||
this.setChainTip(blockDataCopy);
|
||||
synchronized (this.latestBlocks) {
|
||||
BlockData cachedChainTip = this.latestBlocks.peekLast();
|
||||
|
||||
if (cachedChainTip != null && Arrays.equals(cachedChainTip.getSignature(), blockDataCopy.getReference())) {
|
||||
// Chain tip is parent for new latest block, so we can safely add new latest block
|
||||
this.latestBlocks.addLast(latestBlockData);
|
||||
|
||||
// Trim if necessary
|
||||
if (this.latestBlocks.size() >= BLOCK_CACHE_SIZE)
|
||||
this.latestBlocks.pollFirst();
|
||||
} else {
|
||||
if (cachedChainTip != null)
|
||||
// Chain tip didn't match - potentially abnormal behaviour?
|
||||
LOGGER.debug(() -> String.format("Cached chain tip %.8s not parent for new latest block %.8s (reference %.8s)",
|
||||
Base58.encode(cachedChainTip.getSignature()),
|
||||
Base58.encode(blockDataCopy.getSignature()),
|
||||
Base58.encode(blockDataCopy.getReference())));
|
||||
|
||||
// Defensively rebuild cache
|
||||
try {
|
||||
this.stats.latestBlocksCacheRefills.incrementAndGet();
|
||||
|
||||
this.refillLatestBlocksCache();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(() -> "Couldn't refill latest blocks cache?", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.onNewOrOrphanedBlock(blockDataCopy, NewBlockEvent::new);
|
||||
}
|
||||
|
||||
public static class OrphanedBlockEvent implements Event {
|
||||
private final BlockData blockData;
|
||||
|
||||
public OrphanedBlockEvent(BlockData blockData) {
|
||||
this.blockData = blockData;
|
||||
}
|
||||
|
||||
public BlockData getBlockData() {
|
||||
return this.blockData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when we've orphaned a block.
|
||||
* <p>
|
||||
* See <b>WARNING</b> for {@link EventBus#notify(Event)}
|
||||
* to prevent deadlocks.
|
||||
*/
|
||||
public void onOrphanedBlock(BlockData latestBlockData) {
|
||||
// Protective copy
|
||||
BlockData blockDataCopy = new BlockData(latestBlockData);
|
||||
|
||||
synchronized (this.latestBlocks) {
|
||||
BlockData cachedChainTip = this.latestBlocks.pollLast();
|
||||
boolean refillNeeded = false;
|
||||
|
||||
if (cachedChainTip != null && Arrays.equals(cachedChainTip.getReference(), blockDataCopy.getSignature())) {
|
||||
// Chain tip was parent for new latest block that has been orphaned, so we're good
|
||||
|
||||
// However, if we've emptied the cache then we will need to refill it
|
||||
refillNeeded = this.latestBlocks.isEmpty();
|
||||
} else {
|
||||
if (cachedChainTip != null)
|
||||
// Chain tip didn't match - potentially abnormal behaviour?
|
||||
LOGGER.debug(() -> String.format("Cached chain tip %.8s (reference %.8s) was not parent for new latest block %.8s",
|
||||
Base58.encode(cachedChainTip.getSignature()),
|
||||
Base58.encode(cachedChainTip.getReference()),
|
||||
Base58.encode(blockDataCopy.getSignature())));
|
||||
|
||||
// Defensively rebuild cache
|
||||
refillNeeded = true;
|
||||
}
|
||||
|
||||
if (refillNeeded)
|
||||
try {
|
||||
this.stats.latestBlocksCacheRefills.incrementAndGet();
|
||||
|
||||
this.refillLatestBlocksCache();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(() -> "Couldn't refill latest blocks cache?", e);
|
||||
}
|
||||
}
|
||||
|
||||
this.onNewOrOrphanedBlock(blockDataCopy, OrphanedBlockEvent::new);
|
||||
}
|
||||
|
||||
private void onNewOrOrphanedBlock(BlockData blockDataCopy, Function<BlockData, Event> eventConstructor) {
|
||||
requestSysTrayUpdate = true;
|
||||
|
||||
// Notify listeners, trade-bot, etc.
|
||||
EventBus.INSTANCE.notify(new NewBlockEvent(blockDataCopy));
|
||||
EventBus.INSTANCE.notify(eventConstructor.apply(blockDataCopy));
|
||||
|
||||
if (this.notifyGroupMembershipChange) {
|
||||
this.notifyGroupMembershipChange = false;
|
||||
@@ -957,11 +1124,31 @@ public class Controller extends Thread {
|
||||
private void onNetworkGetBlockMessage(Peer peer, Message message) {
|
||||
GetBlockMessage getBlockMessage = (GetBlockMessage) message;
|
||||
byte[] signature = getBlockMessage.getSignature();
|
||||
this.stats.getBlockMessageStats.requests.incrementAndGet();
|
||||
|
||||
ByteArray signatureAsByteArray = new ByteArray(signature);
|
||||
|
||||
BlockMessage cachedBlockMessage = this.blockMessageCache.get(signatureAsByteArray);
|
||||
|
||||
// Check cached latest block message
|
||||
if (cachedBlockMessage != null) {
|
||||
this.stats.getBlockMessageStats.cacheHits.incrementAndGet();
|
||||
|
||||
// We need to duplicate it to prevent multiple threads setting ID on the same message
|
||||
BlockMessage clonedBlockMessage = cachedBlockMessage.cloneWithNewId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(clonedBlockMessage))
|
||||
peer.disconnect("failed to send block");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
|
||||
if (blockData == null) {
|
||||
// We don't have this block
|
||||
this.stats.getBlockMessageStats.unknownBlocks.getAndIncrement();
|
||||
|
||||
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
|
||||
LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
|
||||
@@ -976,10 +1163,19 @@ public class Controller extends Thread {
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
|
||||
Message blockMessage = new BlockMessage(block);
|
||||
BlockMessage blockMessage = new BlockMessage(block);
|
||||
blockMessage.setId(message.getId());
|
||||
|
||||
// This call also causes the other needed data to be pulled in from repository
|
||||
if (!peer.sendMessage(blockMessage))
|
||||
peer.disconnect("failed to send block");
|
||||
|
||||
// If request is for a recent block, cache it
|
||||
if (getChainHeight() - blockData.getHeight() <= BLOCK_CACHE_SIZE) {
|
||||
this.stats.getBlockMessageStats.cacheFills.incrementAndGet();
|
||||
|
||||
this.blockMessageCache.put(new ByteArray(blockData.getSignature()), blockMessage);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while send block %s to peer %s", Base58.encode(signature), peer), e);
|
||||
}
|
||||
@@ -1026,59 +1222,110 @@ public class Controller extends Thread {
|
||||
|
||||
private void onNetworkGetBlockSummariesMessage(Peer peer, Message message) {
|
||||
GetBlockSummariesMessage getBlockSummariesMessage = (GetBlockSummariesMessage) message;
|
||||
byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
|
||||
final byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
|
||||
this.stats.getBlockSummariesStats.requests.incrementAndGet();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
|
||||
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
|
||||
|
||||
do {
|
||||
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
|
||||
|
||||
if (blockData == null)
|
||||
// No more blocks to send to peer
|
||||
break;
|
||||
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
|
||||
blockSummaries.add(blockSummary);
|
||||
parentSignature = blockData.getSignature();
|
||||
} while (blockSummaries.size() < numberRequested);
|
||||
|
||||
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
|
||||
// If peer's parent signature matches our latest block signature
|
||||
// then we can short-circuit with an empty response
|
||||
BlockData chainTip = getChainTip();
|
||||
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
|
||||
Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
blockSummariesMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(blockSummariesMessage))
|
||||
peer.disconnect("failed to send block summaries");
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
|
||||
// Attempt to serve from our cache of latest blocks
|
||||
synchronized (this.latestBlocks) {
|
||||
blockSummaries = this.latestBlocks.stream()
|
||||
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
|
||||
.map(BlockSummaryData::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (blockSummaries.isEmpty()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
|
||||
|
||||
while (blockData != null && blockSummaries.size() < numberRequested) {
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
|
||||
blockSummaries.add(blockSummary);
|
||||
|
||||
blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
|
||||
}
|
||||
} else {
|
||||
this.stats.getBlockSummariesStats.cacheHits.incrementAndGet();
|
||||
|
||||
if (blockSummaries.size() >= getBlockSummariesMessage.getNumberRequested())
|
||||
this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
|
||||
}
|
||||
|
||||
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
|
||||
blockSummariesMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(blockSummariesMessage))
|
||||
peer.disconnect("failed to send block summaries");
|
||||
}
|
||||
|
||||
private void onNetworkGetSignaturesV2Message(Peer peer, Message message) {
|
||||
GetSignaturesV2Message getSignaturesMessage = (GetSignaturesV2Message) message;
|
||||
byte[] parentSignature = getSignaturesMessage.getParentSignature();
|
||||
final byte[] parentSignature = getSignaturesMessage.getParentSignature();
|
||||
this.stats.getBlockSignaturesV2Stats.requests.incrementAndGet();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = new ArrayList<>();
|
||||
|
||||
do {
|
||||
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
|
||||
|
||||
if (blockData == null)
|
||||
// No more signatures to send to peer
|
||||
break;
|
||||
|
||||
parentSignature = blockData.getSignature();
|
||||
signatures.add(parentSignature);
|
||||
} while (signatures.size() < getSignaturesMessage.getNumberRequested());
|
||||
|
||||
Message signaturesMessage = new SignaturesMessage(signatures);
|
||||
// If peer's parent signature matches our latest block signature
|
||||
// then we can short-circuit with an empty response
|
||||
BlockData chainTip = getChainTip();
|
||||
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
|
||||
Message signaturesMessage = new SignaturesMessage(Collections.emptyList());
|
||||
signaturesMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(signaturesMessage))
|
||||
peer.disconnect("failed to send signatures (v2)");
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
List<byte[]> signatures = new ArrayList<>();
|
||||
|
||||
// Attempt to serve from our cache of latest blocks
|
||||
synchronized (this.latestBlocks) {
|
||||
signatures = this.latestBlocks.stream()
|
||||
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
|
||||
.map(BlockData::getSignature)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (signatures.isEmpty()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int numberRequested = getSignaturesMessage.getNumberRequested();
|
||||
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
|
||||
|
||||
while (blockData != null && signatures.size() < numberRequested) {
|
||||
signatures.add(blockData.getSignature());
|
||||
|
||||
blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
|
||||
}
|
||||
} else {
|
||||
this.stats.getBlockSignaturesV2Stats.cacheHits.incrementAndGet();
|
||||
|
||||
if (signatures.size() >= getSignaturesMessage.getNumberRequested())
|
||||
this.stats.getBlockSignaturesV2Stats.fullyFromCache.incrementAndGet();
|
||||
}
|
||||
|
||||
Message signaturesMessage = new SignaturesMessage(signatures);
|
||||
signaturesMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(signaturesMessage))
|
||||
peer.disconnect("failed to send signatures (v2)");
|
||||
}
|
||||
|
||||
private void onNetworkHeightV2Message(Peer peer, Message message) {
|
||||
@@ -1689,4 +1936,8 @@ public class Controller extends Thread {
|
||||
return now - offset;
|
||||
}
|
||||
|
||||
public StatsSnapshot getStatsSnapshot() {
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
||||
// Don't even start trimming until initial rush has ended
|
||||
Thread.sleep(INITIAL_SLEEP_PERIOD);
|
||||
|
||||
int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
||||
@@ -32,12 +34,14 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
||||
if (chainTip == null || NTP.getTime() == null)
|
||||
continue;
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing())
|
||||
continue;
|
||||
|
||||
// Trim blockchain by removing 'old' online accounts signatures
|
||||
long upperTrimmableTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
|
||||
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
|
||||
|
||||
int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
|
||||
|
||||
int upperBatchHeight = trimStartHeight + Settings.getInstance().getOnlineSignaturesTrimBatchSize();
|
||||
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
|
||||
|
||||
@@ -48,16 +52,20 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
||||
repository.saveChanges();
|
||||
|
||||
if (numSigsTrimmed > 0) {
|
||||
final int finalTrimStartHeight = trimStartHeight;
|
||||
LOGGER.debug(() -> String.format("Trimmed %d online accounts signature%s between blocks %d and %d",
|
||||
numSigsTrimmed, (numSigsTrimmed != 1 ? "s" : ""),
|
||||
trimStartHeight, upperTrimHeight));
|
||||
finalTrimStartHeight, upperTrimHeight));
|
||||
} else {
|
||||
// Can we move onto next batch?
|
||||
if (upperTrimmableHeight > upperBatchHeight) {
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(upperBatchHeight);
|
||||
trimStartHeight = upperBatchHeight;
|
||||
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(trimStartHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.debug(() -> String.format("Bumping online accounts signatures trim height to %d", upperBatchHeight));
|
||||
final int finalTrimStartHeight = trimStartHeight;
|
||||
LOGGER.debug(() -> String.format("Bumping online accounts signatures base trim height to %d", finalTrimStartHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ public class Synchronizer {
|
||||
* @throws DataException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
private SynchronizationResult fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight, boolean force, List<BlockSummaryData> blockSummariesFromCommon) throws DataException, InterruptedException {
|
||||
public 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;
|
||||
@@ -313,18 +313,21 @@ public class Synchronizer {
|
||||
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight());
|
||||
|
||||
// Populate minter account levels for both lists of block summaries
|
||||
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
|
||||
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
|
||||
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
|
||||
|
||||
final int mutualHeight = commonBlockHeight - 1 + Math.min(ourBlockSummaries.size(), peerBlockSummaries.size());
|
||||
|
||||
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
|
||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries);
|
||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries);
|
||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
|
||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries, mutualHeight);
|
||||
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
|
||||
|
||||
// If our blockchain has greater weight then don't synchronize with peer
|
||||
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
|
||||
LOGGER.debug(String.format("Not synchronizing with peer %s as we have better blockchain", peer));
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
|
||||
return SynchronizationResult.INFERIOR_CHAIN;
|
||||
}
|
||||
}
|
||||
@@ -405,13 +408,15 @@ public class Synchronizer {
|
||||
Block block = new Block(repository, orphanBlockData);
|
||||
block.orphan();
|
||||
|
||||
LOGGER.trace(String.format("Orphaned block height %d, sig %.8s", ourHeight, Base58.encode(orphanBlockData.getSignature())));
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
--ourHeight;
|
||||
orphanBlockData = repository.getBlockRepository().fromHeight(ourHeight);
|
||||
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onNewBlock(orphanBlockData);
|
||||
Controller.getInstance().onOrphanedBlock(orphanBlockData);
|
||||
}
|
||||
|
||||
LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - applying new blocks from peer %s", commonBlockHeight, commonBlockSig58, peer));
|
||||
@@ -432,6 +437,8 @@ public class Synchronizer {
|
||||
|
||||
newBlock.process();
|
||||
|
||||
LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
@@ -514,6 +521,8 @@ public class Synchronizer {
|
||||
|
||||
newBlock.process();
|
||||
|
||||
LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.qortal.crypto;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
@@ -42,6 +43,27 @@ public abstract class Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 32-byte SHA-256 digest of message passed in input.
|
||||
*
|
||||
* @param input
|
||||
* variable-length byte[] message
|
||||
* @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
|
||||
*/
|
||||
public static byte[] digest(ByteBuffer input) {
|
||||
if (input == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
// SHA2-256
|
||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
sha256.update(input);
|
||||
return sha256.digest();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 message digest not available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 32-byte digest of two rounds of SHA-256 on message passed in input.
|
||||
*
|
||||
|
||||
@@ -3,8 +3,6 @@ package org.qortal.data.block;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BlockSummaryData {
|
||||
|
||||
@@ -14,6 +12,10 @@ public class BlockSummaryData {
|
||||
private byte[] minterPublicKey;
|
||||
private int onlineAccountsCount;
|
||||
|
||||
// Optional, set during construction
|
||||
private Long timestamp;
|
||||
private Integer transactionCount;
|
||||
|
||||
// Optional, set after construction
|
||||
private Integer minterLevel;
|
||||
|
||||
@@ -29,17 +31,23 @@ public class BlockSummaryData {
|
||||
this.onlineAccountsCount = onlineAccountsCount;
|
||||
}
|
||||
|
||||
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount, long timestamp, int transactionCount) {
|
||||
this.height = height;
|
||||
this.signature = signature;
|
||||
this.minterPublicKey = minterPublicKey;
|
||||
this.onlineAccountsCount = onlineAccountsCount;
|
||||
this.timestamp = timestamp;
|
||||
this.transactionCount = transactionCount;
|
||||
}
|
||||
|
||||
public BlockSummaryData(BlockData blockData) {
|
||||
this.height = blockData.getHeight();
|
||||
this.signature = blockData.getSignature();
|
||||
this.minterPublicKey = blockData.getMinterPublicKey();
|
||||
this.onlineAccountsCount = blockData.getOnlineAccountsCount();
|
||||
|
||||
byte[] encodedOnlineAccounts = blockData.getEncodedOnlineAccounts();
|
||||
if (encodedOnlineAccounts != null) {
|
||||
this.onlineAccountsCount = BlockTransformer.decodeOnlineAccounts(encodedOnlineAccounts).size();
|
||||
} else {
|
||||
this.onlineAccountsCount = 0;
|
||||
}
|
||||
this.timestamp = blockData.getTimestamp();
|
||||
this.transactionCount = blockData.getTransactionCount();
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -60,6 +68,14 @@ public class BlockSummaryData {
|
||||
return this.onlineAccountsCount;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public Integer getTransactionCount() {
|
||||
return this.transactionCount;
|
||||
}
|
||||
|
||||
public Integer getMinterLevel() {
|
||||
return this.minterLevel;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.ExecuteProduceConsume;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.google.common.net.InetAddresses;
|
||||
|
||||
@@ -348,21 +349,37 @@ public class Peer {
|
||||
if (this.byteBuffer == null)
|
||||
this.byteBuffer = ByteBuffer.allocate(Network.getInstance().getMaxMessageSize());
|
||||
|
||||
final int priorPosition = this.byteBuffer.position();
|
||||
final int bytesRead = this.socketChannel.read(this.byteBuffer);
|
||||
if (bytesRead == -1) {
|
||||
this.disconnect("EOF");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.trace(() -> String.format("Received %d bytes from peer %s", bytesRead, this));
|
||||
LOGGER.trace(() -> {
|
||||
if (bytesRead > 0) {
|
||||
byte[] leadingBytes = new byte[Math.min(bytesRead, 8)];
|
||||
this.byteBuffer.asReadOnlyBuffer().position(priorPosition).get(leadingBytes);
|
||||
String leadingHex = HashCode.fromBytes(leadingBytes).toString();
|
||||
|
||||
return String.format("Received %d bytes, starting %s, into byteBuffer[%d] from peer %s",
|
||||
bytesRead,
|
||||
leadingHex,
|
||||
priorPosition,
|
||||
this);
|
||||
} else {
|
||||
return String.format("Received %d bytes into byteBuffer[%d] from peer %s", bytesRead, priorPosition, this);
|
||||
}
|
||||
});
|
||||
final boolean wasByteBufferFull = !this.byteBuffer.hasRemaining();
|
||||
|
||||
while (true) {
|
||||
final Message message;
|
||||
|
||||
// Can we build a message from buffer now?
|
||||
ByteBuffer readOnlyBuffer = this.byteBuffer.asReadOnlyBuffer().flip();
|
||||
try {
|
||||
message = Message.fromByteBuffer(this.byteBuffer);
|
||||
message = Message.fromByteBuffer(readOnlyBuffer);
|
||||
} catch (MessageException e) {
|
||||
LOGGER.debug(String.format("%s, from peer %s", e.getMessage(), this));
|
||||
this.disconnect(e.getMessage());
|
||||
@@ -387,6 +404,13 @@ public class Peer {
|
||||
|
||||
LOGGER.trace(() -> String.format("Received %s message with ID %d from peer %s", message.getType().name(), message.getId(), this));
|
||||
|
||||
// Tidy up buffers:
|
||||
this.byteBuffer.flip();
|
||||
// Read-only, flipped buffer's position will be after end of message, so copy that
|
||||
this.byteBuffer.position(readOnlyBuffer.position());
|
||||
// Copy bytes after read message to front of buffer, adjusting position accordingly, reset limit to capacity
|
||||
this.byteBuffer.compact();
|
||||
|
||||
BlockingQueue<Message> queue = this.replyQueues.get(message.getId());
|
||||
if (queue != null) {
|
||||
// Adding message to queue will unblock thread waiting for response
|
||||
@@ -399,7 +423,7 @@ public class Peer {
|
||||
|
||||
// Add message to pending queue
|
||||
if (!this.pendingMessages.offer(message)) {
|
||||
LOGGER.info(String.format("No room to queue message from peer %s - discarding", this));
|
||||
LOGGER.info(() -> String.format("No room to queue message from peer %s - discarding", this));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -454,10 +478,24 @@ public class Peer {
|
||||
while (outputBuffer.hasRemaining()) {
|
||||
int bytesWritten = this.socketChannel.write(outputBuffer);
|
||||
|
||||
LOGGER.trace(() -> String.format("Sent %d bytes of %s message with ID %d to peer %s",
|
||||
bytesWritten,
|
||||
message.getType().name(),
|
||||
message.getId(),
|
||||
this));
|
||||
|
||||
if (bytesWritten == 0)
|
||||
// Underlying socket's internal buffer probably full,
|
||||
// so wait a short while for bytes to actually be transmitted over the wire
|
||||
this.socketChannel.wait(1L);
|
||||
|
||||
/*
|
||||
* NOSONAR squid:S2276 - we don't want to use this.socketChannel.wait()
|
||||
* as this releases the lock held by synchronized() above
|
||||
* and would allow another thread to send another message,
|
||||
* potentially interleaving them on-the-wire, causing checksum failures
|
||||
* and connection loss.
|
||||
*/
|
||||
Thread.sleep(1L); //NOSONAR squid:S2276
|
||||
}
|
||||
}
|
||||
} catch (MessageException e) {
|
||||
|
||||
@@ -34,6 +34,7 @@ public class BlockMessage extends Message {
|
||||
super(MessageType.BLOCK);
|
||||
|
||||
this.block = block;
|
||||
this.blockData = block.getBlockData();
|
||||
this.height = block.getBlockData().getHeight();
|
||||
}
|
||||
|
||||
@@ -93,4 +94,10 @@ public class BlockMessage extends Message {
|
||||
}
|
||||
}
|
||||
|
||||
public BlockMessage cloneWithNewId(int newId) {
|
||||
BlockMessage clone = new BlockMessage(this.block);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -160,80 +160,72 @@ public abstract class Message {
|
||||
/**
|
||||
* Attempt to read a message from byte buffer.
|
||||
*
|
||||
* @param byteBuffer
|
||||
* @param readOnlyBuffer
|
||||
* @return null if no complete message can be read
|
||||
* @throws MessageException
|
||||
*/
|
||||
public static Message fromByteBuffer(ByteBuffer byteBuffer) throws MessageException {
|
||||
public static Message fromByteBuffer(ByteBuffer readOnlyBuffer) throws MessageException {
|
||||
try {
|
||||
byteBuffer.flip();
|
||||
|
||||
ByteBuffer readBuffer = byteBuffer.asReadOnlyBuffer();
|
||||
|
||||
// Read only enough bytes to cover Message "magic" preamble
|
||||
byte[] messageMagic = new byte[MAGIC_LENGTH];
|
||||
readBuffer.get(messageMagic);
|
||||
readOnlyBuffer.get(messageMagic);
|
||||
|
||||
if (!Arrays.equals(messageMagic, Network.getInstance().getMessageMagic()))
|
||||
// Didn't receive correct Message "magic"
|
||||
throw new MessageException("Received incorrect message 'magic'");
|
||||
|
||||
// Find supporting object
|
||||
int typeValue = readBuffer.getInt();
|
||||
int typeValue = readOnlyBuffer.getInt();
|
||||
MessageType messageType = MessageType.valueOf(typeValue);
|
||||
if (messageType == null)
|
||||
// Unrecognised message type
|
||||
throw new MessageException(String.format("Received unknown message type [%d]", typeValue));
|
||||
|
||||
// Optional message ID
|
||||
byte hasId = readBuffer.get();
|
||||
byte hasId = readOnlyBuffer.get();
|
||||
int id = -1;
|
||||
if (hasId != 0) {
|
||||
id = readBuffer.getInt();
|
||||
id = readOnlyBuffer.getInt();
|
||||
|
||||
if (id <= 0)
|
||||
// Invalid ID
|
||||
throw new MessageException("Invalid negative ID");
|
||||
}
|
||||
|
||||
int dataSize = readBuffer.getInt();
|
||||
int dataSize = readOnlyBuffer.getInt();
|
||||
|
||||
if (dataSize > MAX_DATA_SIZE)
|
||||
// Too large
|
||||
throw new MessageException(String.format("Declared data length %d larger than max allowed %d", dataSize, MAX_DATA_SIZE));
|
||||
|
||||
// Don't have all the data yet?
|
||||
if (dataSize > 0 && dataSize + CHECKSUM_LENGTH > readOnlyBuffer.remaining())
|
||||
return null;
|
||||
|
||||
ByteBuffer dataSlice = null;
|
||||
if (dataSize > 0) {
|
||||
byte[] expectedChecksum = new byte[CHECKSUM_LENGTH];
|
||||
readBuffer.get(expectedChecksum);
|
||||
readOnlyBuffer.get(expectedChecksum);
|
||||
|
||||
// Remember this position in readBuffer so we can pass to Message subclass
|
||||
dataSlice = readBuffer.slice();
|
||||
|
||||
// Consume data from buffer
|
||||
byte[] data = new byte[dataSize];
|
||||
readBuffer.get(data);
|
||||
|
||||
// We successfully read all the data bytes, so we can set limit on dataSlice
|
||||
// Slice data in readBuffer so we can pass to Message subclass
|
||||
dataSlice = readOnlyBuffer.slice();
|
||||
dataSlice.limit(dataSize);
|
||||
|
||||
// Test checksum
|
||||
byte[] actualChecksum = generateChecksum(data);
|
||||
byte[] actualChecksum = generateChecksum(dataSlice);
|
||||
if (!Arrays.equals(expectedChecksum, actualChecksum))
|
||||
throw new MessageException("Message checksum incorrect");
|
||||
|
||||
// Reset position after being consumed by generateChecksum
|
||||
dataSlice.position(0);
|
||||
// Update position in readOnlyBuffer
|
||||
readOnlyBuffer.position(readOnlyBuffer.position() + dataSize);
|
||||
}
|
||||
|
||||
Message message = messageType.fromByteBuffer(id, dataSlice);
|
||||
|
||||
// We successfully read a message, so bump byteBuffer's position to reflect this
|
||||
byteBuffer.position(readBuffer.position());
|
||||
|
||||
return message;
|
||||
return messageType.fromByteBuffer(id, dataSlice);
|
||||
} catch (BufferUnderflowException e) {
|
||||
// Not enough bytes to fully decode message...
|
||||
return null;
|
||||
} finally {
|
||||
byteBuffer.compact();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +233,10 @@ public abstract class Message {
|
||||
return Arrays.copyOfRange(Crypto.digest(data), 0, CHECKSUM_LENGTH);
|
||||
}
|
||||
|
||||
protected static byte[] generateChecksum(ByteBuffer dataBuffer) {
|
||||
return Arrays.copyOfRange(Crypto.digest(dataBuffer), 0, CHECKSUM_LENGTH);
|
||||
}
|
||||
|
||||
public byte[] toBytes() throws MessageException {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);
|
||||
|
||||
@@ -90,7 +90,10 @@ public interface ATRepository {
|
||||
/** Returns height of first trimmable AT state. */
|
||||
public int getAtTrimHeight() throws DataException;
|
||||
|
||||
/** Sets new base height for AT state trimming. */
|
||||
/** Sets new base height for AT state trimming.
|
||||
* <p>
|
||||
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
|
||||
*/
|
||||
public void setAtTrimHeight(int trimHeight) throws DataException;
|
||||
|
||||
/** Hook to allow repository to prepare/cache info for AT state trimming. */
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.qortal.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
@@ -139,14 +138,17 @@ public interface BlockRepository {
|
||||
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns block infos for the passed height range, for API use.
|
||||
* Returns block summaries for the passed height range, for API use.
|
||||
*/
|
||||
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException;
|
||||
public List<BlockSummaryData> getBlockSummaries(Integer startHeight, Integer endHeight, Integer count) throws DataException;
|
||||
|
||||
/** Returns height of first trimmable online accounts signatures. */
|
||||
public int getOnlineAccountsSignaturesTrimHeight() throws DataException;
|
||||
|
||||
/** Sets new base height for trimming online accounts signatures. */
|
||||
/** Sets new base height for trimming online accounts signatures.
|
||||
* <p>
|
||||
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
|
||||
*/
|
||||
public void setOnlineAccountsSignaturesTrimHeight(int trimHeight) throws DataException;
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,6 +47,12 @@ public interface Repository extends AutoCloseable {
|
||||
|
||||
public void backup(boolean quick) throws DataException;
|
||||
|
||||
public void checkpoint(boolean quick) throws DataException;
|
||||
|
||||
public void performPeriodicMaintenance() throws DataException;
|
||||
|
||||
public void exportNodeLocalData() throws DataException;
|
||||
|
||||
public void importDataFromFile(String filename) throws DataException;
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.qortal.repository;
|
||||
|
||||
public interface RepositoryFactory {
|
||||
|
||||
public boolean wasPristineAtOpen();
|
||||
|
||||
public RepositoryFactory reopen() throws DataException;
|
||||
|
||||
public Repository getRepository() throws DataException;
|
||||
|
||||
@@ -4,10 +4,21 @@ public abstract class RepositoryManager {
|
||||
|
||||
private static RepositoryFactory repositoryFactory = null;
|
||||
|
||||
public static RepositoryFactory getRepositoryFactory() {
|
||||
return repositoryFactory;
|
||||
}
|
||||
|
||||
public static void setRepositoryFactory(RepositoryFactory newRepositoryFactory) {
|
||||
repositoryFactory = newRepositoryFactory;
|
||||
}
|
||||
|
||||
public static boolean wasPristineAtOpen() throws DataException {
|
||||
if (repositoryFactory == null)
|
||||
throw new DataException("No repository available");
|
||||
|
||||
return repositoryFactory.wasPristineAtOpen();
|
||||
}
|
||||
|
||||
public static Repository getRepository() throws DataException {
|
||||
if (repositoryFactory == null)
|
||||
throw new DataException("No repository available");
|
||||
@@ -35,6 +46,14 @@ public abstract class RepositoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static void checkpoint(boolean quick) {
|
||||
try (final Repository repository = getRepository()) {
|
||||
repository.checkpoint(quick);
|
||||
} catch (DataException e) {
|
||||
// Checkpoint is best-effort so don't complain
|
||||
}
|
||||
}
|
||||
|
||||
public static void rebuild() throws DataException {
|
||||
RepositoryFactory oldRepositoryFactory = repositoryFactory;
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
boolean hadFatalError = resultSet.getBoolean(10);
|
||||
boolean isFrozen = resultSet.getBoolean(11);
|
||||
|
||||
Long frozenBalance = resultSet.getLong(11);
|
||||
Long frozenBalance = resultSet.getLong(12);
|
||||
if (frozenBalance == 0 && resultSet.wasNull())
|
||||
frozenBalance = null;
|
||||
|
||||
@@ -118,7 +118,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
boolean hadFatalError = resultSet.getBoolean(10);
|
||||
boolean isFrozen = resultSet.getBoolean(11);
|
||||
|
||||
Long frozenBalance = resultSet.getLong(11);
|
||||
Long frozenBalance = resultSet.getLong(12);
|
||||
if (frozenBalance == 0 && resultSet.wasNull())
|
||||
frozenBalance = null;
|
||||
|
||||
@@ -147,7 +147,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
bindParams.add(codeHash);
|
||||
|
||||
if (isExecutable != null) {
|
||||
sql.append("AND is_finished = ? ");
|
||||
sql.append("AND is_finished != ? ");
|
||||
bindParams.add(isExecutable);
|
||||
}
|
||||
|
||||
@@ -250,7 +250,8 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
|
||||
String sql = "SELECT state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE AT_address = ? AND height = ? "
|
||||
+ "LEFT OUTER JOIN ATStatesData USING (AT_address, height) "
|
||||
+ "WHERE ATStates.AT_address = ? AND ATStates.height = ? "
|
||||
+ "LIMIT 1";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress, height)) {
|
||||
@@ -272,10 +273,11 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
public ATStateData getLatestATState(String atAddress) throws DataException {
|
||||
String sql = "SELECT height, state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE AT_address = ? "
|
||||
// AT_address then height so the compound primary key is used as an index
|
||||
// Both must be the same direction also
|
||||
+ "ORDER BY AT_address DESC, height DESC "
|
||||
+ "JOIN ATStatesData USING (AT_address, height) "
|
||||
+ "WHERE ATStates.AT_address = ? "
|
||||
// Order by AT_address and height to use compound primary key as index
|
||||
// Both must be the same direction (DESC) also
|
||||
+ "ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
|
||||
+ "LIMIT 1 ";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
|
||||
@@ -306,16 +308,17 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height, state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "JOIN ATStatesData USING (AT_address, height) "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address ");
|
||||
|
||||
if (minimumFinalHeight != null) {
|
||||
sql.append("AND height >= ? ");
|
||||
sql.append("AND ATStates.height >= ? ");
|
||||
bindParams.add(minimumFinalHeight);
|
||||
}
|
||||
|
||||
// AT_address then height so the compound primary key is used as an index
|
||||
// Both must be the same direction also
|
||||
sql.append("ORDER BY AT_address DESC, height DESC "
|
||||
// Order by AT_address and height to use compound primary key as index
|
||||
// Both must be the same direction (DESC) also
|
||||
sql.append("ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
|
||||
+ "LIMIT 1 "
|
||||
+ ") AS FinalATStates "
|
||||
+ "WHERE code_hash = ? ");
|
||||
@@ -337,7 +340,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
bindParams.add(rawExpectedValue);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY height ");
|
||||
sql.append(" ORDER BY FinalATStates.height ");
|
||||
if (reverse != null && reverse)
|
||||
sql.append("DESC");
|
||||
|
||||
@@ -371,9 +374,10 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
@Override
|
||||
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
|
||||
String sql = "SELECT AT_address, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "LEFT OUTER JOIN ATs USING (AT_address) "
|
||||
+ "WHERE height = ? "
|
||||
+ "FROM ATs "
|
||||
+ "LEFT OUTER JOIN ATStates "
|
||||
+ "ON ATStates.AT_address = ATs.AT_address AND height = ? "
|
||||
+ "WHERE ATStates.AT_address IS NOT NULL "
|
||||
+ "ORDER BY created_when ASC";
|
||||
|
||||
List<ATStateData> atStates = new ArrayList<>();
|
||||
@@ -415,45 +419,45 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
@Override
|
||||
public void setAtTrimHeight(int trimHeight) throws DataException {
|
||||
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
|
||||
// trimHeightsLock is to prevent concurrent update on DatabaseInfo
|
||||
// that could result in "transaction rollback: serialization failure"
|
||||
synchronized (this.repository.trimHeightsLock) {
|
||||
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
|
||||
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to set AT state trim height in repository", e);
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||
this.repository.saveChanges();
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to set AT state trim height in repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareForAtStateTrimming() throws DataException {
|
||||
// Rebuild cache of latest, non-finished AT states that we can't trim
|
||||
String dropSql = "DROP TABLE IF EXISTS LatestATStates";
|
||||
|
||||
// Rebuild cache of latest AT states that we can't trim
|
||||
String deleteSql = "DELETE FROM LatestATStates";
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(dropSql);
|
||||
this.repository.executeCheckedUpdate(deleteSql);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to drop temporary latest AT states cache from repository", e);
|
||||
throw new DataException("Unable to delete temporary latest AT states cache from repository", e);
|
||||
}
|
||||
|
||||
String createSql = "CREATE TEMPORARY TABLE LatestATStates "
|
||||
+ "AS ("
|
||||
+ "SELECT AT_address, height FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height FROM ATStates "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
|
||||
+ ") "
|
||||
String insertSql = "INSERT INTO LatestATStates ("
|
||||
+ "SELECT AT_address, height FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height FROM ATStates "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
|
||||
+ ") "
|
||||
+ "WITH DATA "
|
||||
+ "ON COMMIT PRESERVE ROWS";
|
||||
|
||||
+ ")";
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(createSql);
|
||||
this.repository.executeCheckedUpdate(insertSql);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to recreate temporary latest AT states cache in repository", e);
|
||||
throw new DataException("Unable to populate temporary latest AT states cache in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,13 +468,12 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
// We're often called so no need to trim all states in one go.
|
||||
// Limit updates to reduce CPU and memory load.
|
||||
String sql = "UPDATE ATStates SET state_data = NULL "
|
||||
+ "WHERE state_data IS NOT NULL "
|
||||
+ "AND height BETWEEN ? AND ? "
|
||||
String sql = "DELETE FROM ATStatesData "
|
||||
+ "WHERE height BETWEEN ? AND ? "
|
||||
+ "AND NOT EXISTS("
|
||||
+ "SELECT TRUE FROM LatestATStates "
|
||||
+ "WHERE LatestATStates.AT_address = ATStates.AT_address "
|
||||
+ "AND LatestATStates.height = ATStates.height"
|
||||
+ "WHERE LatestATStates.AT_address = ATStatesData.AT_address "
|
||||
+ "AND LatestATStates.height = ATStatesData.height"
|
||||
+ ") "
|
||||
+ "LIMIT ?";
|
||||
|
||||
@@ -488,23 +491,44 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
if (atStateData.getStateHash() == null || atStateData.getHeight() == null)
|
||||
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
|
||||
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates");
|
||||
HSQLDBSaver atStatesSaver = new HSQLDBSaver("ATStates");
|
||||
|
||||
saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
|
||||
.bind("state_data", atStateData.getStateData()).bind("state_hash", atStateData.getStateHash())
|
||||
atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
|
||||
.bind("state_hash", atStateData.getStateHash())
|
||||
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
atStatesSaver.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save AT state into repository", e);
|
||||
}
|
||||
|
||||
if (atStateData.getStateData() != null) {
|
||||
HSQLDBSaver atStatesDataSaver = new HSQLDBSaver("ATStatesData");
|
||||
|
||||
atStatesDataSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
|
||||
.bind("state_data", atStateData.getStateData());
|
||||
|
||||
try {
|
||||
atStatesDataSaver.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save AT state data into repository", e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
this.repository.delete("ATStatesData", "AT_address = ? AND height = ?",
|
||||
atStateData.getATAddress(), atStateData.getHeight());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete AT state data from repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String atAddress, int height) throws DataException {
|
||||
try {
|
||||
this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height);
|
||||
this.repository.delete("ATStatesData", "AT_address = ? AND height = ?", atAddress, height);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete AT state from repository", e);
|
||||
}
|
||||
@@ -514,6 +538,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
public void deleteATStates(int height) throws DataException {
|
||||
try {
|
||||
this.repository.delete("ATStates", "height = ?", height);
|
||||
this.repository.delete("ATStatesData", "height = ?", height);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete AT states from repository", e);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
@@ -355,7 +354,8 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
|
||||
@Override
|
||||
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException {
|
||||
String sql = "SELECT signature, height, minter, online_accounts_count FROM Blocks WHERE height BETWEEN ? AND ?";
|
||||
String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count "
|
||||
+ "FROM Blocks WHERE height BETWEEN ? AND ?";
|
||||
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
|
||||
@@ -368,8 +368,11 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
int height = resultSet.getInt(2);
|
||||
byte[] minterPublicKey = resultSet.getBytes(3);
|
||||
int onlineAccountsCount = resultSet.getInt(4);
|
||||
long timestamp = resultSet.getLong(5);
|
||||
int transactionCount = resultSet.getInt(6);
|
||||
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount);
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount,
|
||||
timestamp, transactionCount);
|
||||
blockSummaries.add(blockSummary);
|
||||
} while (resultSet.next());
|
||||
|
||||
@@ -380,11 +383,11 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException {
|
||||
public List<BlockSummaryData> getBlockSummaries(Integer startHeight, Integer endHeight, Integer count) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT signature, height, minted_when, transaction_count, RewardShares.minter ");
|
||||
sql.append("SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count ");
|
||||
|
||||
/*
|
||||
* start end count result
|
||||
@@ -401,7 +404,6 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
|
||||
if (startHeight != null && endHeight != null) {
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ? AND ?");
|
||||
bindParams.add(startHeight);
|
||||
bindParams.add(Integer.valueOf(endHeight - 1));
|
||||
@@ -413,11 +415,9 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
if (endHeight == null) {
|
||||
sql.append("FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS MaxHeights (max_height) ");
|
||||
sql.append("JOIN Blocks ON height BETWEEN (max_height - ? + 1) AND max_height ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter");
|
||||
bindParams.add(count);
|
||||
} else {
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ? AND ?");
|
||||
bindParams.add(Integer.valueOf(endHeight - count));
|
||||
bindParams.add(Integer.valueOf(endHeight - 1));
|
||||
@@ -432,32 +432,33 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
count = 50;
|
||||
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
|
||||
sql.append("WHERE height BETWEEN ? AND ?");
|
||||
bindParams.add(startHeight);
|
||||
bindParams.add(Integer.valueOf(startHeight + count - 1));
|
||||
}
|
||||
|
||||
List<BlockInfo> blockInfos = new ArrayList<>();
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
return blockInfos;
|
||||
return blockSummaries;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
int height = resultSet.getInt(2);
|
||||
long timestamp = resultSet.getLong(3);
|
||||
int transactionCount = resultSet.getInt(4);
|
||||
String minterAddress = resultSet.getString(5);
|
||||
byte[] minterPublicKey = resultSet.getBytes(3);
|
||||
int onlineAccountsCount = resultSet.getInt(4);
|
||||
long timestamp = resultSet.getLong(5);
|
||||
int transactionCount = resultSet.getInt(6);
|
||||
|
||||
BlockInfo blockInfo = new BlockInfo(signature, height, timestamp, transactionCount, minterAddress);
|
||||
blockInfos.add(blockInfo);
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount,
|
||||
timestamp, transactionCount);
|
||||
blockSummaries.add(blockSummary);
|
||||
} while (resultSet.next());
|
||||
|
||||
return blockInfos;
|
||||
return blockSummaries;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch height-ranged block infos from repository", e);
|
||||
throw new DataException("Unable to fetch height-ranged block summaries from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,13 +478,18 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
|
||||
@Override
|
||||
public void setOnlineAccountsSignaturesTrimHeight(int trimHeight) throws DataException {
|
||||
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
|
||||
// trimHeightsLock is to prevent concurrent update on DatabaseInfo
|
||||
// that could result in "transaction rollback: serialization failure"
|
||||
synchronized (this.repository.trimHeightsLock) {
|
||||
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
|
||||
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to set online accounts signatures trim height in repository", e);
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||
this.repository.saveChanges();
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to set online accounts signatures trim height in repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,16 @@ public class HSQLDBDatabaseUpdates {
|
||||
/**
|
||||
* Apply any incremental changes to database schema.
|
||||
*
|
||||
* @return true if database was non-existent/empty, false otherwise
|
||||
* @throws SQLException
|
||||
*/
|
||||
public static void updateDatabase(Connection connection) throws SQLException {
|
||||
while (databaseUpdating(connection))
|
||||
public static boolean updateDatabase(Connection connection) throws SQLException {
|
||||
final boolean wasPristine = fetchDatabaseVersion(connection) == 0;
|
||||
|
||||
while (databaseUpdating(connection, wasPristine))
|
||||
incrementDatabaseVersion(connection);
|
||||
|
||||
return wasPristine;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,23 +45,21 @@ public class HSQLDBDatabaseUpdates {
|
||||
/**
|
||||
* Fetch current version of database schema.
|
||||
*
|
||||
* @return int, 0 if no schema yet
|
||||
* @return database version, or 0 if no schema yet
|
||||
* @throws SQLException
|
||||
*/
|
||||
private static int fetchDatabaseVersion(Connection connection) throws SQLException {
|
||||
int databaseVersion = 0;
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
if (stmt.execute("SELECT version FROM DatabaseInfo"))
|
||||
try (ResultSet resultSet = stmt.getResultSet()) {
|
||||
if (resultSet.next())
|
||||
databaseVersion = resultSet.getInt(1);
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
// empty database
|
||||
}
|
||||
|
||||
return databaseVersion;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +68,7 @@ public class HSQLDBDatabaseUpdates {
|
||||
* @return true - if a schema update happened, false otherwise
|
||||
* @throws SQLException
|
||||
*/
|
||||
private static boolean databaseUpdating(Connection connection) throws SQLException {
|
||||
private static boolean databaseUpdating(Connection connection, boolean wasPristine) throws SQLException {
|
||||
int databaseVersion = fetchDatabaseVersion(connection);
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
@@ -660,9 +663,10 @@ public class HSQLDBDatabaseUpdates {
|
||||
break;
|
||||
|
||||
case 25:
|
||||
// DISABLED: improved version in case 30!
|
||||
// Remove excess created_when from ATStates
|
||||
stmt.execute("ALTER TABLE ATStates DROP created_when");
|
||||
stmt.execute("CREATE INDEX ATStateHeightIndex on ATStates (height)");
|
||||
// stmt.execute("ALTER TABLE ATStates DROP created_when");
|
||||
// stmt.execute("CREATE INDEX ATStateHeightIndex on ATStates (height)");
|
||||
break;
|
||||
|
||||
case 26:
|
||||
@@ -677,6 +681,96 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS ATTransactionsRecipientIndex ON ATTransactions (recipient)");
|
||||
break;
|
||||
|
||||
case 28:
|
||||
// Latest AT state cache
|
||||
stmt.execute("CREATE TEMPORARY TABLE IF NOT EXISTS LatestATStates ("
|
||||
+ "AT_address QortalAddress NOT NULL, "
|
||||
+ "height INT NOT NULL"
|
||||
+ ")");
|
||||
break;
|
||||
|
||||
case 29:
|
||||
// Turn off HSQLDB redo-log "blockchain.log" and periodically call "CHECKPOINT" ourselves
|
||||
stmt.execute("SET FILES LOG FALSE");
|
||||
stmt.execute("CHECKPOINT");
|
||||
break;
|
||||
|
||||
case 30:
|
||||
// Split AT state data off to new table for better performance/management.
|
||||
|
||||
if (!wasPristine && !"mem".equals(HSQLDBRepository.getDbPathname(connection.getMetaData().getURL()))) {
|
||||
// First, backup node-local data in case user wants to avoid long reshape and use bootstrap instead
|
||||
try (ResultSet resultSet = stmt.executeQuery("SELECT COUNT(*) FROM MintingAccounts")) {
|
||||
int rowCount = resultSet.next() ? resultSet.getInt(1) : 0;
|
||||
if (rowCount > 0) {
|
||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'MintingAccounts.script'");
|
||||
LOGGER.info("Exported sensitive/node-local minting keys into MintingAccounts.script");
|
||||
}
|
||||
}
|
||||
|
||||
try (ResultSet resultSet = stmt.executeQuery("SELECT COUNT(*) FROM TradeBotStates")) {
|
||||
int rowCount = resultSet.next() ? resultSet.getInt(1) : 0;
|
||||
if (rowCount > 0) {
|
||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'");
|
||||
LOGGER.info("Exported sensitive/node-local trade-bot states into TradeBotStates.script");
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("If following reshape takes too long, use bootstrap and import node-local data using API's POST /admin/repository/data");
|
||||
}
|
||||
|
||||
// Create new AT-states table without full state data
|
||||
stmt.execute("CREATE TABLE ATStatesNew ("
|
||||
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_hash ATStateHash NOT NULL, "
|
||||
+ "fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, "
|
||||
+ "PRIMARY KEY (AT_address, height), "
|
||||
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
|
||||
stmt.execute("SET TABLE ATStatesNew NEW SPACE");
|
||||
stmt.execute("CHECKPOINT");
|
||||
|
||||
ResultSet resultSet = stmt.executeQuery("SELECT height FROM Blocks ORDER BY height DESC LIMIT 1");
|
||||
final int blockchainHeight = resultSet.next() ? resultSet.getInt(1) : 0;
|
||||
final int heightStep = 100;
|
||||
|
||||
LOGGER.info("Rebuilding AT state summaries in repository - this might take a while... (approx. 2 mins on high-spec)");
|
||||
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
|
||||
stmt.execute("INSERT INTO ATStatesNew ("
|
||||
+ "SELECT AT_address, height, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
|
||||
+ ")");
|
||||
stmt.execute("COMMIT");
|
||||
}
|
||||
stmt.execute("CHECKPOINT");
|
||||
|
||||
LOGGER.info("Rebuilding AT states height index in repository - this might take about 3x longer...");
|
||||
stmt.execute("CREATE INDEX ATStatesHeightIndex ON ATStatesNew (height)");
|
||||
stmt.execute("CHECKPOINT");
|
||||
|
||||
stmt.execute("CREATE TABLE ATStatesData ("
|
||||
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_data ATState NOT NULL, "
|
||||
+ "PRIMARY KEY (height, AT_address), "
|
||||
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
|
||||
stmt.execute("SET TABLE ATStatesData NEW SPACE");
|
||||
stmt.execute("CHECKPOINT");
|
||||
|
||||
LOGGER.info("Rebuilding AT state data in repository - this might take a while... (approx. 2 mins on high-spec)");
|
||||
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
|
||||
stmt.execute("INSERT INTO ATStatesData ("
|
||||
+ "SELECT AT_address, height, state_data "
|
||||
+ "FROM ATstates "
|
||||
+ "WHERE state_data IS NOT NULL "
|
||||
+ "AND height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
|
||||
+ ")");
|
||||
stmt.execute("COMMIT");
|
||||
}
|
||||
stmt.execute("CHECKPOINT");
|
||||
|
||||
stmt.execute("DROP TABLE ATStates");
|
||||
stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates");
|
||||
stmt.execute("CHECKPOINT");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -59,6 +60,8 @@ public class HSQLDBRepository implements Repository {
|
||||
protected List<String> sqlStatements;
|
||||
protected long sessionId;
|
||||
protected final Map<String, PreparedStatement> preparedStatementCache = new HashMap<>();
|
||||
// We want the same object corresponding to the actual DB
|
||||
protected final Object trimHeightsLock = RepositoryManager.getRepositoryFactory();
|
||||
|
||||
private final ATRepository atRepository = new HSQLDBATRepository(this);
|
||||
private final AccountRepository accountRepository = new HSQLDBAccountRepository(this);
|
||||
@@ -183,8 +186,20 @@ public class HSQLDBRepository implements Repository {
|
||||
|
||||
@Override
|
||||
public void saveChanges() throws DataException {
|
||||
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
this.connection.commit();
|
||||
|
||||
if (this.slowQueryThreshold != null) {
|
||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||
|
||||
if (queryTime > this.slowQueryThreshold) {
|
||||
LOGGER.info(() -> String.format("[Session %d] HSQLDB COMMIT took %d ms", this.sessionId, queryTime), new SQLException("slow commit"));
|
||||
|
||||
logStatements();
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("commit error", e);
|
||||
} finally {
|
||||
@@ -208,7 +223,7 @@ public class HSQLDBRepository implements Repository {
|
||||
this.savepoints.clear();
|
||||
|
||||
// Before clearing statements so we can log what led to assertion error
|
||||
assertEmptyTransaction("transaction commit");
|
||||
assertEmptyTransaction("transaction rollback");
|
||||
|
||||
if (this.sqlStatements != null)
|
||||
this.sqlStatements.clear();
|
||||
@@ -298,11 +313,12 @@ public class HSQLDBRepository implements Repository {
|
||||
Path oldRepoDirPath = Paths.get(dbPathname).getParent();
|
||||
|
||||
// Delete old repository files
|
||||
Files.walk(oldRepoDirPath)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
try (Stream<Path> paths = Files.walk(oldRepoDirPath)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.filter(file -> file.getPath().startsWith(dbPathname))
|
||||
.forEach(File::delete);
|
||||
}
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
// Nothing to remove
|
||||
@@ -342,11 +358,12 @@ public class HSQLDBRepository implements Repository {
|
||||
Path backupDirPath = Paths.get(backupPathname).getParent();
|
||||
String backupDirPathname = backupDirPath.toString();
|
||||
|
||||
Files.walk(backupDirPath)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
try (Stream<Path> paths = Files.walk(backupDirPath)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.filter(file -> file.getPath().startsWith(backupDirPathname))
|
||||
.forEach(File::delete);
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
// Nothing to remove
|
||||
} catch (SQLException | IOException e) {
|
||||
@@ -355,12 +372,21 @@ public class HSQLDBRepository implements Repository {
|
||||
|
||||
// Actually create backup
|
||||
try (Statement stmt = this.connection.createStatement()) {
|
||||
stmt.execute("BACKUP DATABASE TO 'backup/' NOT BLOCKING AS FILES");
|
||||
stmt.execute("BACKUP DATABASE TO 'backup/' BLOCKING AS FILES");
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to backup repository");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkpoint(boolean quick) throws DataException {
|
||||
try (Statement stmt = this.connection.createStatement()) {
|
||||
stmt.execute(quick ? "CHECKPOINT" : "CHECKPOINT DEFRAG");
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to perform repository checkpoint");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performPeriodicMaintenance() throws DataException {
|
||||
// Defrag DB - takes a while!
|
||||
@@ -374,8 +400,34 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportNodeLocalData() throws DataException {
|
||||
try (Statement stmt = this.connection.createStatement()) {
|
||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'MintingAccounts.script'");
|
||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'");
|
||||
LOGGER.info("Exported sensitive/node-local data: minting keys and trade bot states");
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to export sensitive/node-local data from repository");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importDataFromFile(String filename) throws DataException {
|
||||
try (Statement stmt = this.connection.createStatement()) {
|
||||
LOGGER.info(() -> String.format("Importing data into repository from %s", filename));
|
||||
|
||||
String escapedFilename = stmt.enquoteLiteral(filename);
|
||||
stmt.execute("PERFORM IMPORT SCRIPT DATA FROM " + escapedFilename + " STOP ON ERROR");
|
||||
|
||||
LOGGER.info(() -> String.format("Imported data into repository from %s", filename));
|
||||
} catch (SQLException e) {
|
||||
LOGGER.info(() -> String.format("Failed to import data into repository from %s: %s", filename, e.getMessage()));
|
||||
throw new DataException("Unable to export sensitive/node-local data from repository: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns DB pathname from passed connection URL. If memory DB, returns "mem". */
|
||||
private static String getDbPathname(String connectionUrl) {
|
||||
/*package*/ static String getDbPathname(String connectionUrl) {
|
||||
Pattern pattern = Pattern.compile("hsqldb:(mem|file):(.*?)(;|$)");
|
||||
Matcher matcher = pattern.matcher(connectionUrl);
|
||||
|
||||
@@ -411,11 +463,12 @@ public class HSQLDBRepository implements Repository {
|
||||
LOGGER.info("Attempting repository recovery using backup");
|
||||
|
||||
// Move old repository files out the way
|
||||
Files.walk(oldRepoDirPath)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
try (Stream<Path> paths = Files.walk(oldRepoDirPath)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.filter(file -> file.getPath().startsWith(dbPathname))
|
||||
.forEach(File::delete);
|
||||
}
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
// Now "backup" the backup back to original repository location (the parent).
|
||||
@@ -455,6 +508,10 @@ public class HSQLDBRepository implements Repository {
|
||||
if (this.sqlStatements != null)
|
||||
this.sqlStatements.add(sql);
|
||||
|
||||
return cachePreparedStatement(sql);
|
||||
}
|
||||
|
||||
private PreparedStatement cachePreparedStatement(String sql) throws SQLException {
|
||||
/*
|
||||
* We cache a duplicate PreparedStatement for this SQL string,
|
||||
* which we never close, which means HSQLDB also caches a parsed,
|
||||
@@ -504,7 +561,7 @@ public class HSQLDBRepository implements Repository {
|
||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||
|
||||
if (queryTime > this.slowQueryThreshold) {
|
||||
LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
|
||||
LOGGER.info(() -> String.format("[Session %d] HSQLDB query took %d ms: %s", this.sessionId, queryTime, sql), new SQLException("slow query"));
|
||||
|
||||
logStatements();
|
||||
}
|
||||
@@ -597,7 +654,7 @@ public class HSQLDBRepository implements Repository {
|
||||
long queryTime = System.currentTimeMillis() - beforeQuery;
|
||||
|
||||
if (queryTime > this.slowQueryThreshold) {
|
||||
LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
|
||||
LOGGER.info(() -> String.format("[Session %d] HSQLDB query took %d ms: %s", this.sessionId, queryTime, sql), new SQLException("slow query"));
|
||||
|
||||
logStatements();
|
||||
}
|
||||
@@ -791,15 +848,15 @@ public class HSQLDBRepository implements Repository {
|
||||
if (this.sqlStatements == null)
|
||||
return;
|
||||
|
||||
LOGGER.info(() -> String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId));
|
||||
LOGGER.info(() -> String.format("[Session %d] HSQLDB SQL statements leading up to this were:", this.sessionId));
|
||||
|
||||
for (String sql : this.sqlStatements)
|
||||
LOGGER.info(sql);
|
||||
LOGGER.info(() -> String.format("[Session %d] %s", this.sessionId, sql));
|
||||
}
|
||||
|
||||
/** Logs other HSQLDB sessions then returns passed exception */
|
||||
public SQLException examineException(SQLException e) {
|
||||
LOGGER.error(String.format("HSQLDB error (session %d): %s", this.sessionId, e.getMessage()), e);
|
||||
LOGGER.error(() -> String.format("[Session %d] HSQLDB error: %s", this.sessionId, e.getMessage()), e);
|
||||
|
||||
logStatements();
|
||||
|
||||
@@ -833,14 +890,19 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
|
||||
private void assertEmptyTransaction(String context) throws DataException {
|
||||
try (Statement stmt = this.connection.createStatement()) {
|
||||
String sql = "SELECT transaction, transaction_size FROM information_schema.system_sessions WHERE session_id = ?";
|
||||
|
||||
try {
|
||||
PreparedStatement stmt = this.cachePreparedStatement(sql);
|
||||
stmt.setLong(1, this.sessionId);
|
||||
|
||||
// Diagnostic check for uncommitted changes
|
||||
if (!stmt.execute("SELECT transaction, transaction_size FROM information_schema.system_sessions WHERE session_id = " + this.sessionId)) // TRANSACTION_SIZE() broken?
|
||||
if (!stmt.execute()) // TRANSACTION_SIZE() broken?
|
||||
throw new DataException("Unable to check repository status after " + context);
|
||||
|
||||
try (ResultSet resultSet = stmt.getResultSet()) {
|
||||
if (resultSet == null || !resultSet.next()) {
|
||||
LOGGER.warn(String.format("Unable to check repository status after %s", context));
|
||||
LOGGER.warn(() -> String.format("Unable to check repository status after %s", context));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -848,7 +910,11 @@ public class HSQLDBRepository implements Repository {
|
||||
int transactionCount = resultSet.getInt(2);
|
||||
|
||||
if (inTransaction && transactionCount != 0) {
|
||||
LOGGER.warn(String.format("Uncommitted changes (%d) after %s, session [%d]", transactionCount, context, this.sessionId), new Exception("Uncommitted repository changes"));
|
||||
LOGGER.warn(() -> String.format("Uncommitted changes (%d) after %s, session [%d]",
|
||||
transactionCount,
|
||||
context,
|
||||
this.sessionId),
|
||||
new Exception("Uncommitted repository changes"));
|
||||
logStatements();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
|
||||
|
||||
private String connectionUrl;
|
||||
private HSQLDBPool connectionPool;
|
||||
private final boolean wasPristine;
|
||||
|
||||
/**
|
||||
* Constructs new RepositoryFactory using passed <tt>connectionUrl</tt>.
|
||||
@@ -65,12 +66,17 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
|
||||
|
||||
// Perform DB updates?
|
||||
try (final Connection connection = this.connectionPool.getConnection()) {
|
||||
HSQLDBDatabaseUpdates.updateDatabase(connection);
|
||||
this.wasPristine = HSQLDBDatabaseUpdates.updateDatabase(connection);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Repository initialization error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasPristineAtOpen() {
|
||||
return this.wasPristine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepositoryFactory reopen() throws DataException {
|
||||
return new HSQLDBRepositoryFactory(this.connectionUrl);
|
||||
|
||||
@@ -65,6 +65,7 @@ public class Settings {
|
||||
"::1", "127.0.0.1"
|
||||
};
|
||||
private Boolean apiRestricted;
|
||||
private String apiKey = null;
|
||||
private boolean apiLoggingEnabled = false;
|
||||
private boolean apiDocumentationEnabled = false;
|
||||
// Both of these need to be set for API to use SSL
|
||||
@@ -83,6 +84,10 @@ public class Settings {
|
||||
private long repositoryBackupInterval = 0; // ms
|
||||
/** Whether to show a notification when we backup repository. */
|
||||
private boolean showBackupNotification = false;
|
||||
/** How long between repository checkpoints (ms). */
|
||||
private long repositoryCheckpointInterval = 60 * 60 * 1000L; // 1 hour (ms) default
|
||||
/** Whether to show a notification when we perform repository 'checkpoint'. */
|
||||
private boolean showCheckpointNotification = false;
|
||||
|
||||
/** How long to keep old, full, AT state data (ms). */
|
||||
private long atStatesMaxLifetime = 2 * 7 * 24 * 60 * 60 * 1000L; // milliseconds
|
||||
@@ -271,6 +276,9 @@ public class Settings {
|
||||
// Validation goes here
|
||||
if (this.minBlockchainPeers < 1)
|
||||
throwValidationError("minBlockchainPeers must be at least 1");
|
||||
|
||||
if (this.apiKey != null && this.apiKey.trim().length() < 8)
|
||||
throwValidationError("apiKey must be at least 8 characters");
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -319,6 +327,10 @@ public class Settings {
|
||||
return !BlockChain.getInstance().isTestChain();
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
public boolean isApiLoggingEnabled() {
|
||||
return this.apiLoggingEnabled;
|
||||
}
|
||||
@@ -430,6 +442,14 @@ public class Settings {
|
||||
return this.showBackupNotification;
|
||||
}
|
||||
|
||||
public long getRepositoryCheckpointInterval() {
|
||||
return this.repositoryCheckpointInterval;
|
||||
}
|
||||
|
||||
public boolean getShowCheckpointNotification() {
|
||||
return this.showCheckpointNotification;
|
||||
}
|
||||
|
||||
public long getAtStatesMaxLifetime() {
|
||||
return this.atStatesMaxLifetime;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
@@ -23,7 +22,6 @@ import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Serialization;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
@@ -334,27 +332,20 @@ public class BlockTransformer extends Transformer {
|
||||
|
||||
public static byte[] getBytesForMinterSignature(BlockData blockData) throws TransformationException {
|
||||
byte[] minterSignature = getMinterSignatureFromReference(blockData.getReference());
|
||||
PublicKeyAccount minter = new PublicKeyAccount(null, blockData.getMinterPublicKey());
|
||||
|
||||
return getBytesForMinterSignature(minterSignature, minter, blockData.getEncodedOnlineAccounts());
|
||||
return getBytesForMinterSignature(minterSignature, blockData.getMinterPublicKey(), blockData.getEncodedOnlineAccounts());
|
||||
}
|
||||
|
||||
public static byte[] getBytesForMinterSignature(byte[] minterSignature, PublicKeyAccount minter, byte[] encodedOnlineAccounts)
|
||||
throws TransformationException {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(MINTER_SIGNATURE_LENGTH + MINTER_PUBLIC_KEY_LENGTH + encodedOnlineAccounts.length);
|
||||
public static byte[] getBytesForMinterSignature(byte[] minterSignature, byte[] minterPublicKey, byte[] encodedOnlineAccounts) {
|
||||
byte[] bytes = new byte[MINTER_SIGNATURE_LENGTH + MINTER_PUBLIC_KEY_LENGTH + encodedOnlineAccounts.length];
|
||||
|
||||
bytes.write(minterSignature);
|
||||
System.arraycopy(minterSignature, 0, bytes, 0, MINTER_SIGNATURE_LENGTH);
|
||||
|
||||
// We're padding here just in case the minter is the genesis account whose public key is only 8 bytes long.
|
||||
bytes.write(Bytes.ensureCapacity(minter.getPublicKey(), MINTER_PUBLIC_KEY_LENGTH, 0));
|
||||
System.arraycopy(minterPublicKey, 0, bytes, MINTER_SIGNATURE_LENGTH, MINTER_PUBLIC_KEY_LENGTH);
|
||||
|
||||
bytes.write(encodedOnlineAccounts);
|
||||
System.arraycopy(encodedOnlineAccounts, 0, bytes, MINTER_SIGNATURE_LENGTH + MINTER_PUBLIC_KEY_LENGTH, encodedOnlineAccounts.length);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new TransformationException(e);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static byte[] getBytesForTransactionsSignature(Block block) throws TransformationException {
|
||||
|
||||
719
src/main/resources/block-212937-deltas.json
Normal file
719
src/main/resources/block-212937-deltas.json
Normal file
@@ -0,0 +1,719 @@
|
||||
[
|
||||
{ "address": "Qa43JP9hnNjfSy1f3LYYNFhhSuMokUoYqQ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qa4VMxDhmGH5dgYLiuFSyaWju8xb2fGZhs", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qa8pRawmcviX1BHQpNCt4vBYHz7HjdNfkL", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QacFHzkV265jd57jfTZ5gSuW8dj4W1ttYs", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QadVAZb3yc78yjQwQDJ8bXCvFohAdEovu7", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QaeFXCfi73Wptwve5R2RdFSUJUs2dqsHXY", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qaf5BHpXrWKK3dprQX7zcCXtCPiQdhm7oo", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QaK6URQq4vwEDWyBtmS25kor49Z56An7xn", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QakzYX9JZyUjRYtXJeaQbirXTuUqMFdp7d", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QaLWoAkjc7ip5Y38p5FX8vVbEYFHCz2zHh", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QanYq81HNrintpSE6FPRVioHfhCHzTkN83", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QaoVMdbpPQDHfBAar1HfPoEBzDoFa2PjS8", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QapE6pVuceYdVKnHVePbeaqf5QNeoDKbqr", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QaPKuyyQtXJcsVhKLKgxCcYewxwaawxLrB", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QaPKuyyQtXJcsVhKLKgxCcYewxwaawxLrB", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "Qaq4sV9wkdSSSgJtaPuuSV15tqsVPD78rz", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qar2d2pXBP7NCe8mNTbzTSzAQ48ptGqrMC", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QasrD9TxGAuWqnRpJxBBwwh7Nj6BHiA77d", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QaSXJSHbQ4xmwaQd5tKJMAqvVzwzLvhddP", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qata5oApMShnD4F1kcgSJMTiYsxTPSFW4F", "assetId": 0, "balance": 0.00001283},
|
||||
{ "address": "QaUciVnbQDXdNygJadEY31PuDEBLi6Spmu", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QaUciVnbQDXdNygJadEY31PuDEBLi6Spmu", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QavBkY3kRPJxtvsU5yWuhUnMdDWvs4E3Dw", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QavzMF32Xvbg4Q4rM9a9R5WVmQZt7iW4Fa", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QawB5MesBratjs2d9EMnXnrN4EC7gw7LRw", "assetId": 0, "balance": 0.00000015},
|
||||
{ "address": "QawB5MesBratjs2d9EMnXnrN4EC7gw7LRw", "assetId": 2, "balance": 0.00000004},
|
||||
{ "address": "QawSgZ7i2LLFTKyPxQptk9gN526ihy5yZi", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QaxMTV7fGSnibyvVjRaX7FRerrb9aMW6SR", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QaywytB5dqQoDgBejmEhWXWaDgfL1CRBGH", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QaZs97g4Mbq9tXMoBWbhw3jFvBBVkWKS5F", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qb1pkXG4xufNS3ki354CWkEmC1gmz6D2H7", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qb2VbWdrY2E9uLALmyan35E6H5ze6tBmxX", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qb5F6KX4Fg1LRM21QJF48m1EYxnipFfRy1", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qb9Ycc3f6KUyWPMBGgeEczy4HorPFfy2hj", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QbBMsJvjo4ZouPPGegxRs5kqKuRzfLRYMU", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QbchhqR3QLE1T1kRzySFWsVamhPy8oyeC1", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qbcy4uyMkQF2JXYqGkueDiFNZ4tHjRg8CR", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QbdV2vipqMui1eQnj9ZudQgw4e8zRgQ9Lk", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qbh6gWsxNtcaKq5sAq4NVkCTXuqyA6pbUm", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbJhMqYk94FExie11Vs5y5x7CNUS5e5b5W", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QbJJho6sTHnqL2ECivtfUrYZTuEgemEha2", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbJqEntoBFps7XECQkTDFzXNCdz9R2qmkB", "assetId": 0, "balance": 0.00000103},
|
||||
{ "address": "QbLPi1Ac6zZGTLURapA6YxyiFYVXH6uZYQ", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QbmJDoAJ9cjRNM9AuMv5AZc4w83kqosCYS", "assetId": 0, "balance": 0.00000004},
|
||||
{ "address": "QbMppCoLPnXdzBQBCqXaD1iCBLGKVSW7Z1", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QbNaKP3udSoKqgRdVR5uik5tb2QrgmyY5w", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QbnizezPemhpQg1roAMs9MAvJVw4KHiSuq", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qbp8CZLnBwphPGwqJGm91LTaFJ4mkXZLgg", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QbPGzVLU7B9TSrr3fde2u7xoyQfRUpk8st", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qbq1ctcvwnkChPmU9PiH4fAExbgTv3fBm3", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbQQhQM7XdoGPPjJG1ffNwGEU4tmgUVSyb", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbqRyFw7Xu6Nsb4FraaUSe7nUPukuUpekG", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QbSLbuAxRMqe9vdQpmhbannkJcieXgPfnY", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qbtut4Z8a37Mokd5uvsA54WXfBHN1Ho1Kx", "assetId": 0, "balance": -0.00006713},
|
||||
{ "address": "QbUPbjTu3NpEZQcJp4JcTarLRon4oTiSqi", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbUSTCbTkdKgaMJuKiEfJfa32cXTy4sHkr", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QbvxC3ENqomXp11833APchdjeyCNd49nLj", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QbXjh5buBXW68AmJBUW2URV4YnM59vMhkP", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbxJvwrEHZs7MDE8rbqBwZAZkcywue5F3W", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbY4qRTuHee7gX93n7RJytxNXJhMeQcdCP", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QbYaYDYjTDohUtsbALeR4PQPUXL2qYe3hh", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbYTowTHCr9WzfrR6b8uDfJKwL41nG1vyr", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbYVbsJ99wWEDNn7fGNgUYuSN1fk6y3T1x", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbyVFRE1zKKcprNvpCBx1VfEh9uosYZojs", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QbzERVYhUEJKvWRVpEeiacV9HcNxjoCzA4", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "Qc1wMMJbivCnM4QjvgJDWqmSQYUfuswhts", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qc54dt4km6NrxBMvtEX51jKiuNmHmzEuee", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qc9dZchoYfc1eRJhSLXR9rxSHcqNB47Dex", "assetId": 0, "balance": 0.00000026},
|
||||
{ "address": "QcCBVfL35rxSyQ416L2MBz14FYbNrbeNPx", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QcCL2sk1nLLE99HgqjGpqLQbCwPtSozBx5", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qce2Djqrk2WzG1QhMZ3BqFok9HGsz4wtM3", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QcEpMZ9NUkLcEv2aWw6FPu9f58CSKVSH8N", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "Qcf5wVLGjgdt57kYsn1D5H6TuWorqb6hww", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QcfhhsQG9vgVdQULu8RrXaXooJUec1xMj1", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QcFZ9yCvGESF7gi12jt9XF8RY423c6RfLm", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QcGdwubfgY14EPjc7peuWn7vz8tKZbWQw4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QcGFjReZ7yjNJaCMF1SfXbdrPCGeZhdgCv", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QcgxbHijQFUaBBD1Xv6k2Fjh2hRPp2AUFg", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QcHF9YogbuzZhG4fK4116pgE2qrmbkGh2n", "assetId": 0, "balance": 0.00000004},
|
||||
{ "address": "QcHF9YogbuzZhG4fK4116pgE2qrmbkGh2n", "assetId": 2, "balance": 0.00000004},
|
||||
{ "address": "QcJwVCyzraPy51uB4xd4f94n2UFYAsznGC", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qck27pE28zWwmMoxa2hypGa9X6rhjBBfmJ", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QckLxn2NgwZZjV92W8VKnHUWiWUVmQrhiJ", "assetId": 0, "balance": -0.00001256},
|
||||
{ "address": "QcNmqT8CZ6zSZwuRm5LahRZnuGBJRnPY8o", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QcPPNyDKGk5vQfPEpXQQKvYeidvBZ9T7nS", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QcPro2T97Q8cAfcVM4Pn4fv71Za4T6oeFD", "assetId": 0, "balance": 0.00000103},
|
||||
{ "address": "QcQPQmeU1hw8fWQmsBCKEuxg3kRizaQYUz", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QcqTj1unHj5FXExKu7RpRJHPBMPjGtmXtJ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QcrnYL6yNwHKuEzYLXQ8LewG3m2B5k9K5f", "assetId": 0, "balance": 0.00000019},
|
||||
{ "address": "QcrnYL6yNwHKuEzYLXQ8LewG3m2B5k9K5f", "assetId": 2, "balance": 0.00000015},
|
||||
{ "address": "QcRYGiF4ffxMUq3CGNrcFP646KbeCcnK66", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QcU4VhU9ohDXU4k4AUMapgJRYSzEpizjLN", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QcUA6GT9FiPBbeE7ttBXu1avBHZzDsZg2o", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QcwEgPdsvF1TugqnHvT2bwXLCLzEKMVk3A", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qcx4PE9bn3qXn88XhpDmNSBGS32SmDE8Ds", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QcXuXDcqzq8goJAqwambRU2Uk9RQ513mV9", "assetId": 0, "balance": -0.00003608},
|
||||
{ "address": "QcyBacxzvdMP5votSnAJyA39fu9BgYhWmG", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qd1Px9vhWuEmF2SbLx3Ez7HhGtifGMa8TJ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qd1Xw41BzN1CgASqsh2PcrrkTKyDs2MVYF", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "Qd1Xw41BzN1CgASqsh2PcrrkTKyDs2MVYF", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "Qd33zAmKqm89UWMes6bfRMMoSNjasehzzX", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qd3bVidnA4fhKv1xwHcKsDZC3MUBFhkrUa", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qd453ewoyESrEgUab6dTFe2pufWkD94Tsm", "assetId": 0, "balance": 0.00000103},
|
||||
{ "address": "Qd75TjafsrikBgnfA6Hb6Y9wk45LwiavB1", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qd7rmD8PZvKKyJLr4qgvFQzeLPRhYkKcya", "assetId": 0, "balance": -0.00001256},
|
||||
{ "address": "Qdb27GFXfyWFDKY3urtrKrQRshkeL8hWgt", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QdFZk74skMUu4rKMPEmcSVwR87LNDe6o3Y", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QdGbhtkFHUqd9nK9UegxxGXD1eSRYSoKjt", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QdgbtYSRsDgKZ2PZMKCfoNWfGuvDm2idmP", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QdHc49iRMiCaanZfD8kGiTZaZxJneDTU7j", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QdkheUawBwuvhD5J5N21uqypH1hZw1enGE", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QdkTGqDYde3Y9Q6EgxmBrJGAK2jm4HXspX", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QdmFGD2ef9gdkUbXpNBuKVkbGGnBRBoReS", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QdP6twdTsJpq3eLDgi83t6LH367gauJLqo", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QdRzjsQrz8edqeRNX7VcASbSz2hPfvX783", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QdsakiJEhKaKGtG4ue2k5xJdt6kYsxwPba", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QdsiBFfTQUPMrS6NuWdcYCU94t8M64EcPf", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QdsMQUyuWyYT5Sit8YSMW9bKjBhfq8MwRY", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QdSQjxAdRwfg4JgdaXNp5CZwTFL8ARwDJf", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QdtAQm1EGNgM7QDSaC2qvV9WdpRHwpApUT", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QdTY1v63aMSibfPV2sJTAJZu2mqDP4dMZV", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QdudYG9SDw5WYzfoj9oq3QC4abHx8ZWCce", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QdwSxr3t4hdGHjQFy6EVGR9yGMipefsTuo", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QdxDaHTEX5cUg4S6ohMAi6mE8wQZCqQBoT", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QdXdUxnyKGGo7eEfTcx85oEikNe5nYnuwa", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QdXe21sjY8smjVmiAUgZY8xWVzwgxMgK5A", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QdXe21sjY8smjVmiAUgZY8xWVzwgxMgK5A", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QdXMNPHhbt2kiwkp7NPskBsrZudxZ6gXN2", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QdyzBSPBNLfyCfdPNBn86EcXUZeDdkCYLm", "assetId": 0, "balance": 0.00096104},
|
||||
{ "address": "QdZ5Krd84CX6oh6jorEhYyL7zdCacrPAj2", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qe29bjnmk29z19Nw3xBkbWMqMzy7SkzZ57", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qe7RxFfsV5JNkQNuK9UVvtTQMXhcCcTTTf", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qe9S8zA27FPdPVVLcVQj9noaKsuwySPKdq", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qe9VPzQp3h4Kg3DHSHBUQ3AM3AiRBfCDfX", "assetId": 0, "balance": 0.00000019},
|
||||
{ "address": "Qe9VPzQp3h4Kg3DHSHBUQ3AM3AiRBfCDfX", "assetId": 2, "balance": 0.00000015},
|
||||
{ "address": "QeAa7yawpJqQYk7PNisVD89HezskBRecH6", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QeaDGU85fpffwsw9ngmd98QsT6NaFyFFed", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QeaDGU85fpffwsw9ngmd98QsT6NaFyFFed", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QeAHiq28seiCm7wxMoo4NWJtAoBVMtZrpc", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QeaJHCCTy7AebbPeF1scsBLbezcBHAKtKt", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QeAwxFMkYMmTJN5dysZtYAaq2SAxjeYrL4", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "Qec83tt9eX6Ng41GE8PU91GWMi72Hk74K5", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QeCTKHwG4zypj1bV7uNAzyMc4hwed2EFga", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QeH2ajmr3ca3t2g6xcnbmFeGYd9BeACvA8", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QekAWuiw9PUQfygRRF31aLemzHBLkRWpiU", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QepkE9dJdWsYZZdYRP5bV4NbzQnpABWR4m", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qeq85FoJpxtzoDM93WiNQQCXiuiFynRQzm", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "Qeq85FoJpxtzoDM93WiNQQCXiuiFynRQzm", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QeSh3t1AnaRcRThkkUTvvdMEouixCADeVh", "assetId": 0, "balance": 0.00000033},
|
||||
{ "address": "QeSh3t1AnaRcRThkkUTvvdMEouixCADeVh", "assetId": 2, "balance": 0.00000033},
|
||||
{ "address": "QesUoX7rrugxqGFCk4AYntQkoxvXcLpEoS", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QeSzLpUw9as4LUHTJ3CNK6SW9okCkU1qMG", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QeTgFSAQj6AihCoJ5cfNJt2ZCDkSUGSAnB", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QeU4z63x84mZJmwjxZLKWkJbRu46iP1H2z", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QeUE2MQKGopfmrcLknKfrDnJ8ddoktxrHr", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QeVGUYQSiVgAq4mZ2KQswbkkxBxxH3jb9Q", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QewrEYLQ7anM7UyPvLEEitQpfD4pjt1pQQ", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QeY5cbodjaunb2anyhQMwurmZtZUuuDCc1", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QeZm1XBbyycGh8mdcoBTGpD2Z4v5unA2yt", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QezTNFB9czsSYhbJN9YMLNrhRNtmazJrfC", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qf2eC5PFMzfqdcS5xq4vfE4n8Hmt3iuYuk", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qf3gdKYQqKgXs6sAVkyW2uHkautxNbzgvJ", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "Qf4QdaFNUN7m3eKJfst1vzq87n3cgER6gT", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qf9AMghvwAMwoRi38YzDNNxQHrEwAwgyNo", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QfbX8JJupEw5ckNtU4upQgET35oLTr5e6v", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qfd3gxberH9K6ipiV33VH3TUooNFYyV1iu", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QfdMLMLZC9Kt15DRAdxwNfaYdGBEMF9Sb4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qffd1qyWXmC5ZgUc4GQzPCHkeH29DS52H4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QfhJM5CpX5MhfjCcopwfw7pgS6w1hVJ49E", "assetId": 0, "balance": 0.00000037},
|
||||
{ "address": "QfhJM5CpX5MhfjCcopwfw7pgS6w1hVJ49E", "assetId": 2, "balance": 0.00000037},
|
||||
{ "address": "QfjL32jLsxtumbfx6ufmfCFCBccVCQFkrh", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QfJVbN5dRnMUSedZH68HhYCTJzjbUo121Q", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QfKsnQFFJWMkXEz2bdSuB734uMNpi5VAaD", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QfmM8dgfikTB2FYVuJ9owzQXVm8wP7T4QT", "assetId": 0, "balance": 0.00000014},
|
||||
{ "address": "QfnbnWrRQ4HNDQvtg3wG2B1eC4ycUsFqZz", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QfPcwetW3BErP4ySTurxFJSHpNkNXPEhGk", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QfPcwetW3BErP4ySTurxFJSHpNkNXPEhGk", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QfpUpwgV5h6SQqaywGvxvBzzV9D774993x", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qfr6suiRoJVGWgmxrAb5sdZVWWuPm1aXqD", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QfRykgxR139CmZ4nDjmFQvaSmNiv576ZYT", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qft1Ckorj3uckvuTjokt6k2FB15oNkVW5a", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "Qft1ktvJ14eBFjpJaphT24ks4WRcN3K6tB", "assetId": 0, "balance": 0.00000022},
|
||||
{ "address": "Qft1ktvJ14eBFjpJaphT24ks4WRcN3K6tB", "assetId": 2, "balance": 0.00000022},
|
||||
{ "address": "QfTxdUv4M5LWuaoybQwh1VZ8843Wqq1r1t", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QfUu2KAEuoxBKHMNFMKaryyoX7vRTSvCFP", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qfw8iJLRok3vFrenNxt2o4DatGY3hThsmr", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QfWuDf4QE2ygs3mT1nokqqgQgFfgYm2BMo", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qfxkjavp3UR7tuG988Hau1PF3Um27fU6VX", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qg1yzP82SghJT7o3kgcWMMF6UYYb6BS8c1", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qg2273uygJvXdtp2kmjWgPEy48nX56ctZj", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qg32vHmYCUq16co8mj4Ljb8bWzWu2eWmyF", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qg6B3mCqHBUY6jm6fL2ddtUvWTZea3xcfV", "assetId": 0, "balance": -0.00003608},
|
||||
{ "address": "Qg7TPhUD5sns2pJiLjxnktRAx71WXsNp9y", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qg9d4zDLvzhqqjDvRz99jiFoRnygTyzeZg", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qga1LWYfbXkBaNm5LCh4HwSW8L3g8MFzSm", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgBfw49fpZzCL7FRLwMsq8677ffZLk1XBa", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgcF6KgVZ9eDAMHJdSEeAtp91t931VKZMv", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgCQq4cFaGrJhwvKs4XwvccKiLZ8GVMCXR", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QgECFJiiri2dDN4zA32URvbdDid2cFrJwM", "assetId": 0, "balance": 0.00000051},
|
||||
{ "address": "QgECFJiiri2dDN4zA32URvbdDid2cFrJwM", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QgEGaSaoCj1bGxyj35qaZcpb23Px2bBJmq", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QgesuKa3zwx8VAseF1oHZAFHMf29k8ergq", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qgfh143pRJyxpS92JoazjXNMH1uZueQBZ2", "assetId": 0, "balance": 0.00000103},
|
||||
{ "address": "QgFjfyApbjupAa1PLBdy5NGNZWXEha1p9T", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QggJ9Rnh99rRJMjsp6nLSuR4m8FAve8Wfe", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QgHhee3CSRavmr7h87XSsLb3esiQUyRjxj", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QgHRqFHrhTDL4TmnzXpQDtno4q24Q24uL9", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgHsU3UbVH2HWd3cZKsivtCTMcZjsyEYjc", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QgJdTosTZQPzBYiWQSeCDw5zGWxa96zfkA", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgkGF35JZnfzzzZ3GcrLjdiE7DWGzsoLGz", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QgmEtScSZWJmTUAidCZKj6gDr3LznZ6rr4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgmJAg1X3MaQ1kp8ABKe7j6okY3RdumNfE", "assetId": 0, "balance": -0.00003608},
|
||||
{ "address": "QgNtUZEQAbDAn2zbBVu8KLexZHLvDN3Rcw", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgnZT74VfseKiSPZgzMBVE7JpRs3N2dMs2", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qgp16aMcdiS2EUkxCm5NSZgB8DixGK51zT", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgpXUK9QEyJgFedP68iSPqD91CwoRnpB6X", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgqM5bKs3tNqKNAnVeaQp4oaYMXCmX6YJr", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QgRfZM6pz7JoX8N3YheCqZCLkZbLZmAzKQ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgSCgLQWuMCd9867ygUToidDaHstCaaK7X", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgsifXqfJtdsNbxqGYh3hExEpWWZDg9rKK", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QgsxkxTBhBwtccex56cwbaYydp3imnikJe", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QguDWvRKfdRv1bHDV5wqqnY1drJQTn6365", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgUhCiEHoA8ERQFzog8ubuCd321f1VYbDP", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QgVRwXN3x9suBrh7Dc1HPnRiMeuLqZ5FJk", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QgVZb632eqF1eLQm9gBGuBtyp9Dyz2FKUK", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QgYQpgDWSMi6Rma7VqzYsuG7TWq1ChSxEv", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QgZAyh4znJgzsb5tKGsYXXKhaZ2zYitqVg", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qh2cPc2Bn8fceg3wCCvkSk38oEkQ6KxWaG", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qh4EmrLoRwePL6mi9XZ85s1d2pkkfzj3RV", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qh55kmNyvRAmA7KZPAiZ5gmJSLGNNAxL5m", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QhCcvRt4jmyFtjeqeHGeU4Z1DKdRFGmxs3", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhCyt8DqiumxXXJka9ErkieUWGW5AA8SvD", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QhD2RCxdxXRKku893rvdtJbnv1bt2QR5TD", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QhdoF3Kt3dV5DkuPgTmvH3RNzgZrSK9o6W", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QhDPwTaDTzjSCwYHZvphyqRVhHYZ9CWmzz", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QheP4ZKWsYzu14fewMdUayA4rkXpuH8a4p", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhErhzUapqPRDdXYyaS9nG8cvbCibhRUpq", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QhH8txpLcffTmttkYjS9AWqi2Vwz6hHCBp", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhjbKpRtrTHegRr28KHVgP6XAZyJyjkapw", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhkyUQcRTxSDzTzUGJoPGnLC9N1ZiDcwnt", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhM6LS7TCiAiWMbvXWMWSDNJVEwHPdLb94", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhPwExMZk8mW4FvH2HtQGbq5mU2sTHNS9B", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhPXuFFRa9s91Q2qYKpSN5LVCUTqYkgRLz", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhQdzLn36SDgrgoMfvdZAkoWtTUHpB3acJ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhqoEihESYJnVDTBWpEPsXub6c7eCJgAma", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhQsFX4iYf9f5zQp5CLQPQVzSEX2fTcSbx", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QhsactZ9HZTkUSff3fWpRNxSZjueunYiF1", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhspjBT3mpnao5EeLqEY3HJFXv42uPpCks", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhTHNmR4YgYJu5o2uzGkgUHFpg1pYu9KU3", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qhuos9t2XkBCmiFiroQFwQ7CaULAZ9YBnj", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhwZ6thwxwaucJfbNxB2LoA17GZqfaA1D7", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QhYD58sT9N8b6jhNgcmVnLXRuVCnbnuxUd", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QhYS1Ag1RjYVUSGYYKyvQXSRWN9nyFGnnS", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhZJFejortmvM99apbw83n9RVFhFpNUCLF", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QhzyudB9g5TieSbCi2SBtm9sS8ia2hq4oe", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qi1W3hiPZWH6wfGt2imicU7upZcqHy7RBv", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "Qi2Cw4zZFvHLQnZVhwjM1ygqbn6nDEB4ZN", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qi3N6fNRrs15EHmkxYyWHyh4z3Dp2rVU2i", "assetId": 0, "balance": 0.00000096},
|
||||
{ "address": "Qi73xBLZg3PMJELtLd9GebkDCW7Kjk1juc", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "Qi8EEW1qUuG63yRShzKBh1Wb7r88UeCNZZ", "assetId": 0, "balance": -0.00001256},
|
||||
{ "address": "Qi8j8NLi2wBg7JUAb9qwctXwbyLbmbN6pp", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QiA5SLN3NASu4EStTmCAzvR4ih1Z1TwhwC", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QibuD4c6gvXgS4iut7q3sXuVb23rgFJq2M", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QiBYApdEYRwsFYjt59UJqZV55wcwykvhsh", "assetId": 0, "balance": 0.00000015},
|
||||
{ "address": "QiBYApdEYRwsFYjt59UJqZV55wcwykvhsh", "assetId": 2, "balance": 0.00000004},
|
||||
{ "address": "QicRwDhfk8M2CGNvpMEmYzQEjESvF7WrFY", "assetId": 0, "balance": 0.00000040},
|
||||
{ "address": "QiDji6mSFEF3PjGnKfZvMwJrR1GQtnf6Pd", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QiERDpXv985tgbL39GsKrrkbrfmKBj6bpN", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QiFUUj4GvfHTTuAhFseuoWZm3wYemqxSDn", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QiGkwhUZJRsg1AzcQofw78KVmbGeoobTyf", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QiGN3Kce81GdoiWkztj58hypZ1qBUiMPnZ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QiGumRw6CnrTkWKkAp7pXYSBvtNsDPaoGH", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QihzNPXWQC5HqjmzqT91GzhNpVXmveGJq6", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QiJQdet8ziyDeCijhJXFE7MnWbX5XQpn2T", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QiQUssukhoo1ft4G9Mxa8JpViqFW4PdBjJ", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QiRDWHPRQcp6jQrtjqNYRDtkvGnsjryXF5", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qis1kSR77JUx957Lw4oV1kZsHAtZ6bL52W", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QiSRUnc4FbFX4Mb4b8i2Aa9ebXb6r3qhNr", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QisSQZ7Et7Rfzx2SCC2o9UDSeRZWMyFKWc", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qit8cfptq7zJXj9xiZRGoT8Lz36TeLjcsS", "assetId": 0, "balance": -0.00006713},
|
||||
{ "address": "QiYuVgjzYfEwobgrTFVsSa2R3ut4tL2rm1", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qj2USguA2xGYbUFHwU9jzJwmyGCiTaQEcS", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "Qj6NdK4qoLrsHkWoDNhasSkrLLsudFMDWp", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qj6nk7RCJcB6gB5SZYGnKrqk6umyVD2XWT", "assetId": 0, "balance": 0.00000028},
|
||||
{ "address": "QjBMFFPhQryK31Uk7jzhLaC4grbq4Lv3XM", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QjdMUSewptx5M9KXUrx8HPSVZPXqa8JDVC", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QjEaMxcBKMsj91ytKe6GdTBJP8Mu1Ru3r4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QjEAs2or122weKppv5zALzoQzXxbsDjy3f", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "Qjf1sJh7Y7e16QWjExgzQt7o2Mqxrgw977", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "Qjfqk23ZTP9wNLTPLyQzewhxgmjxh8cwv7", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QjgGeEkyiXa43pyqkXxZbvAChQpVYfUyKz", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QjKRbeTYYi53Pu4Ph3t8seavsoay3N8zpi", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QjKzZNNuBYWvZkGP1YtTnbPXY12DPmhWcP", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QjLMLprtMqwfKEN3XoocfDRVSWkbKQwm7d", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QjMWr9osCo2eJVZyzRn5zNURn6azCC4Agx", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QjQosFc13zX2kN52Miyo3DuBYU288jNkdW", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QjrC8NXwR8gFkEauvRwCPxqHroPFqAJbhK", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QjrC8NXwR8gFkEauvRwCPxqHroPFqAJbhK", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QjrCFCi6dqvka4UELg2SHhM2oWnQWepd1o", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QjTRoy39Bfq1DJD6UPiHvcCVgrA663WkSf", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QjVm1ZaT62korzr9XvRxmJppyFF4sdafeT", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QLcnnJygHWRvCYyxk1EUwMMWpJicgp8WkF", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QLdw5uabviLJgRGkRiydAFmAtZzxHfNXSs", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QLfUgN4QRkHacD6PdxfUUQUBG7NXkqz2Pg", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QLhxpFjnYi8HToiHep6X3okP1U45bpz54S", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QLi8RY1wquju2jXpEgJ1f9e2i1NyBQhxJy", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QLj3L7YAX1TqCBvkMcXJ7pKoVyVwhjhhMA", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QLjE5xQbBcALTSpnuu3Ey5SG7jqj4ke8hZ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QLk4souHeUSaT5jKezcmKtjUexZKyqXjqb", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QLqSs841jDXCJQ1RJ4xq68V5V2FQtP9GkU", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QLr2d2rVviocEfqta6cRb9uKZfH8YEGb2P", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QLu1ZhFYAHdq5YPemwBBy6wNtJS9ZnsiV4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QLwMaXmDDUvh7aN5MdpY28rqTKE8U1Cepc", "assetId": 0, "balance": 0.00000103},
|
||||
{ "address": "QLxSSDU5QNfpSzWpBuLSauuj6uEecqUcD1", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QM1jwcy9hgFmjkbNHcJTv9ksS6q3gpeHNd", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QM7gZsy5p5qxuyikPEqMUcUArDHtJ2A1Kv", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QM9zVbXXnfrtQ1X7zPQ5zxPYPAWTaVMXqZ", "assetId": 0, "balance": 0.00000036},
|
||||
{ "address": "QM9zVbXXnfrtQ1X7zPQ5zxPYPAWTaVMXqZ", "assetId": 2, "balance": 0.00000022},
|
||||
{ "address": "QMC3FQSa83vDUH3ApyoVnJe6P8JTdedCiR", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QMEFWMLuPfmFzzGH5WCECz4VE6HjenLic9", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMFG3ucD5qJXShA9uzD3gVnhfKfTnNnJpX", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMfWg9oJg49izXMeRWrsErgNnBD6mJcKiX", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMGgED2eawpZZNoRTGghwkMP9NLUoCYoVw", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMH8qGMEHvw9YPUCE2fX9recgGfSWh6Aao", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QMHbcfCReXRZPd1xaRaFMdFdKXXUsLC3nc", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMiGaZbcWXdj61Rn1UGVAPgg8s31puuX1v", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMJwdufHY9dMoARHCUyGbMPAqUB4BcqGKm", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMMh94Pfs5LVE4xJee1yggViqP1YDdQHT4", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QMNfNWsJuFwiSufnVwWpGU7bqcNgdTKF7o", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMozpRT9aUunfmPh7EtQ6LPoth2JFJWBXC", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMpb5Gxr9PTReeCN6r3BZgPMXozMpmmaQM", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMPNM6p7zADqo2hp4DTXdPEZLgKTQ7qJJr", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QMQKzygMix6WVy2J1kdepSSHjJnk2nK6MK", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMSfC5v8AF5SntsKuKnsgLt2EbaMNWvNhz", "assetId": 0, "balance": -0.00001256},
|
||||
{ "address": "QMsKXQAYKmR7dBH4P3kMLiKzYatK3h1CeS", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QMt6UeGcA8BLkLkfAjy8AShnBtu8ECgooc", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QMtm8wVPHGE3qHg2hMaj6SZ78D5eXw3VWZ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMuWNAJ2tbeViHtBUN3yD2KARrrzcanLAd", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMyV3ZofyJrRTmZfpoHrPoNZo7oA9vnoZY", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QMz6NGa9TZzChT1sMRXjiM7uR5q6Nn34Fb", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMzbEtBrrjBDoFFz6n7bd6uzLWy3iXeZhN", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QMZJe6ZxJ7rVQmX2nUqH6JdUAXXyJTvJHu", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QN289qYSyeBD5jS3vKnqHZ4nNnpqbeh2n6", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QN8RijNFo7SDDKYgF5yiWuh86UhWpcpdGL", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QN95wtwKG2yT7NZkpU1q1QmFpNSYcdQVZL", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QN9E7MDY914bqaw13TxLrs47iPzW9rJF8e", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QNAVKy8r1WXTSisFcdPjwwYHsbMW479MwX", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNb5Za9VKLMni3MzyzuxxjSAykXbPGSVU1", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNCHqRw177Ct9ExD7FiAJaN6w6yhqkNuiY", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNEF6iVnzXPAqhJf4x46DhXSnRrPjgqWiC", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNfi5TZ8LYdK1acZz2VKnChvhY5t7QRWe1", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNfmBzXcb3gLXmBteM4oToqakRrCFXjVuS", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNFsnWD53JWrQJzVAj4JWr75qS3Pu4MMXk", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QNHBFkVUopmpYyguufnSe6DUcbThtEUu47", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QNHdGeFJmPcDdN8prPzPL4bk2dpnJ2ZZFr", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNiTnonHpXTeUrgNdyYWVDPP4ZdjkLpW72", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNiuxkAEGm1a5K3EzPhEvnFHVCQQWuoEoh", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QNMDKE7XTujNQkuQorcHXw6hL7qRvyaTjr", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QNMZnDAbYDzKLYRVNpJFcEnr3411rQBguw", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QNoRAk6XmihoFnqP6SCEFNhR66n3McCaTF", "assetId": 0, "balance": -0.00006713},
|
||||
{ "address": "QNoxXt2xDKrM51adtcFLcW92wk615qA64H", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNqBFbZXg5STiW6F4Lj1a1v8dMaFiACpJR", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNrYqn8pMjx8ax7jBeQa6onzrjEEapCABf", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNsiHhrAQUDk5h3ecLw8bAiF2179aggSsK", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNuSfDpB84q9Xydrpk7Rhu5mNY5BfWSVcc", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QNVKrjEq5bZdiDtgo64m5kz87rTHqCwvCP", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QNw21XRyVhudVTc15XcZZ7giKGWVAndSig", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNw9xAm9TUerin9QsapCPL9mV6zmoXyJrh", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNwVgYAQZxc6KD9FMUT2dmQBLaXdnxv7yF", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNXCHtt6hwppn3DjKVHEn2ybmPehgNGuV8", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QNzMU13YxVuueojNCXegU3cDXUfju7TLkB", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QP2hds2BNPhsK7fyMHgGApzQDuGbjhEwbD", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QP3J3GHgjqP69neTAprpYe4co33eKQiQpS", "assetId": 0, "balance": 0.00000099},
|
||||
{ "address": "QP5fR7t4SJE6F5U2q2gPLEh9U63GCEr4pB", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QP7yGYN9fJufzuFWRSjwUXhdnVokypQ3Es", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QP8rHEbqCr8D1aUHEn3rKa2Jgtahcjf6We", "assetId": 0, "balance": -0.00006713},
|
||||
{ "address": "QP8xG56L8b28h1mguSk9LuzNhxbHgAoL9b", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QP91M2haBGwDcayzvGp7wBBM1pugC5Sse1", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QP9vU5yTsBjuTSFxH5Cb9VXYNRHKhMNAJ4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QP9y1CmZRyw3oKNSTTm7Q74FxWp2mEHc26", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPcTWoAhYWmwjmWbQAS8muisrQVaLJMbg7", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QPcTWoAhYWmwjmWbQAS8muisrQVaLJMbg7", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QPEbvVBWDG7qgy4smY8nWiie78Vec8qiT9", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QPEd3HwgZ9w6W9eYnEMuS4NmjD8iR3DMQM", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPfP9syFgebRP5A1s2DK7kC1L6hWFoLjoB", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QPGvKDAhG86Z9UDyo6pSvDLUQkSCi44JfT", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPLoqpwAoytvpQKwvJ6GRsaRcVZ3xnYgVB", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPmNcXdK2EVmxZ2KSeGS5N6s8Koikwnutc", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPnfdCQNDDP4LTpUQPEiydgmp734mXNvb5", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QPNuXTLsQaBzUTENu4mmhLyqTAKAVfdVym", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPpr2JS24d9maQtXLpNQuLqivWe17VNth8", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPQaXAcVz6jtP9X5oCwUhHrC6PY7jpof7r", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPqfuZpmyA6cK6WUFwcGeKH2Te1aegkHBM", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPqXoYhTPiDdSuwcAj9JvrnBuzTYDBJEmv", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QPREQjU2defiYdgA33HDiLNGBpxtuebeqE", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QPrh9z8gNmRe5SU2zmBHSbZzXawkHDiDwy", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPRJwmpAh2A9ed1V4ib2GYat4XEZTebPqr", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QPsjHoKhugEADrtSQP5xjFgsaQPn9WmE3Y", "assetId": 0, "balance": 0.00000082},
|
||||
{ "address": "QPUMyJ59kkrp75tDzDPxSyw1GWCrbC2cS2", "assetId": 0, "balance": 0.00000004},
|
||||
{ "address": "QPusqAVBVFGAAeE7RdospttA18AuyLP7sB", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPV6pAxUghP23w3KDEv3PDcD9EvAypdvbJ", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QPVCG6EUkxcuznnDRf4aDLUNSUTnWiKeA7", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPwESkkM7hCQ8gSa3cgY3sXB27cQMMrkU9", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QPWxc25kgMu2ZsFnZwGz8yXdSbNnmgig6s", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPWzseBZj9UDGTASKeub5QTGwhpvTvGrAf", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPYBoSu8KdPoNGkpjZo7FNy6br2Etzx7q9", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPYfRd1uhnAgqkZNmjNCjgPhkguMnHWuc4", "assetId": 0, "balance": 0.00000022},
|
||||
{ "address": "QPYfRd1uhnAgqkZNmjNCjgPhkguMnHWuc4", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QPyx2bNiAnJEjitfeAh8jZXzQVKio2B7Mi", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QPZJVL5PD7ZDEWa2TfN6nx8h55MraPk6SR", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQ5qnof5pUgJem8NPsAPgYdENL88cNqSj9", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQ6FA4TgpqPc8kN4Sp9LVUZ7Wcix4kT3rc", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQ9VZH256J59hAQaHvbqB2DJDPfkvo8R2U", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QQa3MTgdnru5B7wSqPcq7qXcZcpbDQ7oyE", "assetId": 0, "balance": 0.00000004},
|
||||
{ "address": "QQaKBSjAt9RK2bqJoSriR77X4ULstGzrFQ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQAuaqYCU2XfTuCkNn4KPbNA7txNN2om62", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQbzLNiPHMqtrjGYuHXNgED4F6Pc89t7am", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QQEWYGZBbmdLL4HrQrAtnyCdzsm67GxAhr", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QQEZEGWt3sAPwEWYD2RQ6tMwnpkayG81dY", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QQFabMW4DtU23uUhZRe47Q4F4h2uTHvgcq", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QQfd4mHdR3YvUXgtq1t6s5RxbnVdagqLiY", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQfwxmBGXXU6U88DeYqpp9k3j99g5deGD4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQjiCpwLkxEdvYa5EQvrKoxAL6dA6uJCq7", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQoENGwx2bpj24aF9cuGUFd7GVWPH8Led3", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQPYyoE3Bm2vh8Wr5aaBNyirC8dd3BhBGH", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QQrnqFh6AedkwRSAEzWWJUfLVtJPbfNurK", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQRwKAbAtVFVbydiwAFmoUivVMPxrND78o", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQuhcRELLCkgcc8UTGXKLQfMGY5RWMKwf3", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQXgH4CnQCB76BbXhsApu6ShhohFfvoXv7", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQYp1TiGWbRChHY8fWzeNSYrBSbyczwkcK", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QQzMut6erjgSKCpZ1dHDcjKcj9KAce7cug", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QR5xsQro2R42oU1bcXoZoqxqBsaKvoZkPG", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QR9UR5QUE7yAwPyios25WQdworma6k8iLf", "assetId": 0, "balance": -0.00006713},
|
||||
{ "address": "QRaDef6H2zYfefqLwYGmUg7T6DAqo6DDqc", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRCkZ5zUcgo7mMthsYznkbjxRgeqyKDKtD", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QRCtc67FTNKS5zVXM8omw8F55h9DP7herL", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QRctujZqsh51nbvfxmJzXcCoJDA5hRxdmp", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRcvSzSNezuGkLqAoeAHqVvQDGUP6CTKeq", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QREtYDhP4HkpeCCZroemuGXMGVFoZHH3Lp", "assetId": 0, "balance": 0.00003643},
|
||||
{ "address": "QREtYDhP4HkpeCCZroemuGXMGVFoZHH3Lp", "assetId": 2, "balance": 0.00000015},
|
||||
{ "address": "QRFHr4jnVgvAsPTubeSrh8bPy1yzwzYaWD", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QRHCneJApSW2qe2uuo1QkFq5Xb4qx5YfpK", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QRHj3FnBzzDM314JVi4HNcvAHit5EXamLo", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QRiAaFKLPgScKPUjGHEA5uTa1gjt8ZRXSv", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QRJRPTC431ortbmXizawh3JM64Vd1qGWhu", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRk5TG57SQGLkybXUqxBnobADTFGj9GR3Z", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRKRk5HVADsN1LHygK7q2pA7dWnYKnPpCT", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QRmdkrmtJrjXyrGLGEPB981KLw5GddvXgw", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QRMYnBhWD1ncLWiMrMeRiMpcTnc4dv3sTb", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QRnLRt2D4hkKFsyxq2UUfUH5mGwchJc25h", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRP5BTeMLUqWbkES3gRqFHsWh4ey8Bot2v", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRQa5tidBjxWd29s8Rqvmcv7Lm1irtPnio", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QRrvsUPv9Xv6EL9M3uEWiJiSMDV4uQc1zv", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QRt11DVBnLaSDxr2KHvx92LdPrjhbhJtkj", "assetId": 0, "balance": 0.00000103},
|
||||
{ "address": "QRtRELSSASzqiYy2FtNcrePH6TVnqJkv9B", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QRtwMe8xmGic45KkXJ2mADFmbLq4fnnY4g", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QRu2rd4V1wnQ8yifhk18JgCTyvpoZMTg8j", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRUbzEbLd7fRjAx2fBdXAH4QS1WQyetvDc", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRViAhwyGycgNbRZ4ywQHEWtVDcN6L1e5q", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QRWEbcRnLoGccAndtLcGgpeQFH2ZBcMqHo", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRWEbzH4niUcu9dL3Yq42X4j89aqQk3qWw", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRwxvvk5UBLjwYQTXxZG1Xa8yYssGKTUKj", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QRZWZQP7Tmi4orAWcHWhXpfmjtK4TdCuSu", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QS1i9K7iJb49TA4w43VSC3fEURF6bRXvw9", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QS2wQpMWBXaN8tV1hvWoJVSXtFKoJ4jBDJ", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QS3kGsoFhyPeeyCbQcGiMH3LvP2KYNaKxe", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QS49qPLS7w4xqBdcbYqwUwKjY2wN7AVmxu", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QS7K9EsSDeCdb7T8kzZaFNGLomNmmET2Dr", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSApBAn8pD6jmLs6j4WwxeCa341Crb9yp4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSAtkxer4LwkdQqzStB82K74CNSXuamx8x", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSbHwxaBh5P7wXDurk2KCb8d1sCVN4JpMf", "assetId": 0, "balance": 0.00000103},
|
||||
{ "address": "QSBVtqNoaM9pfi8zMgMY9pTQhaF6XjMUZU", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QScBgSw74MquesXmVJxerX3YgyhtShRr4q", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSE7sy84kVtiB5tQiRWcSXZmQX5NG3tM1k", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSFhD2auWxoBqBzMZggf1FqTzoUxz7cddo", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSGB4Rd2xhd6UmA9LALTQ4f89Tfsz5VajU", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QSgFEURyKLVh8z84Wxb6MJxDSvY7DaRuUM", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QSH7dFCpRkbxvfrAeAxK81u5HyBbgbUHs9", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSHLf7MR3LtKN5oeWewqJPEmgMBDVRB6Pb", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSJmwFYNx8mGn1n791WFzJW8BqJqZZZwRt", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSjqYaBA8euuKZsvJQu9moQmaPkPgjnxUL", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QSKaxQHPYatp6YcE33CmtwiovP1qZAJZSe", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSkicapNH35a3UebSxxSMCfntBhwwi6veW", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSMZpdZWbMZQa7wxcywzrzaWTQTN216mjk", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSq7VrrognfVmGLrPhmpRVZZmHw5LApnD5", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QSq8y4ZrSbF55ZddWNcw1ett2LDtjQEvNn", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSU5p6Q78dQM44hCDCJwnWgbVyXPg9wMH4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QSUruudcrhmPuM9v4JAoSnAdeQpFjQUtwG", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QSwwCXx9hJgM7ZAVvFb1oQjrMcSfgBcDqy", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QSZSkfeNcaK2fKLJiF6TwVuZuEt4opALN4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QT2X4TSA8mG7UitNFwY5DkkV7WS1RRPn7R", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QT5PXfTynkrhckdqd6L5NyGxeWVBgXtMQC", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QT6K1KJ3ED3wm7Fdc2ETW27spHWdjYhAXG", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QTBG5F778g7j2yw82ReDZuAqyLC3xe1RCu", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTbrzCB9nnAv7Vno5dEAw4NXxAfVoNwyWA", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTd6P8ZuoG36VRE9W3VhtvRWQgHN3qTkhT", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QTdSGHWUaEjx1kW1AZdRAZPCkaNwcqDCPe", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QTdWujAFt2ErKotw4cjiorqLUqCiWCMz9i", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QTecbuir4YPxLQ9c9Ht1TVrkHTKfnAALBd", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTEE4ZJXv68ke4841HWjTLAAU8mfccxwbE", "assetId": 0, "balance": 0.00000004},
|
||||
{ "address": "QTFg4go5uJ1oZidqRCXqu7miyUKiqzWuD2", "assetId": 0, "balance": 0.00169901},
|
||||
{ "address": "QTGBUbMv9cKMxbrrCQBiXtj6XUEyYumNns", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QTGeQqn3XEFdnnCqvifCFXYdKym7SaHzTd", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QTgtYSdWErieArhJ7eznKSv451TqCxMYxa", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTKKxJXRWWqNNTgaMmvw22Jb3F5ttriSah", "assetId": 0, "balance": 0.00000103},
|
||||
{ "address": "QTM9jb15o2kU9fT6ARQdYJGWfH5xC1vtCt", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QTMczbPVBQ4Yvr3GdjS6YeLjRCBn8hvx68", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QTMTFswUU83XVmk6T4Gez7qUJCccbAad7S", "assetId": 0, "balance": 0.00000004},
|
||||
{ "address": "QTpYQqRyMekaEuECziirzy3HvCVofZS1wJ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTRAvkMBrHEt4sDYAa6dHUNeGjmcfAYtys", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTrJucEocNy7MHXvVqWWN82TGnm7ssue2H", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QTSi7WDsgJtCmGpE9vJot32dmozM21bDrR", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTSrDNbWFxFUzpCX9MnoGuBKgwP1oQqjsg", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QTsutcJRvjMV8MhTvuetGL6rPEAnvcdYZB", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTTrv8SWR8huV8TFYUEQhfZ1j1JmtL5p8G", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QTTrv8SWR8huV8TFYUEQhfZ1j1JmtL5p8G", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QTtXS6fZGThRLq4qgkwM4ngBYkLoFyZ3bK", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTVLRwoopfg7qSG9CMfCPJz3UydnT3jDxD", "assetId": 0, "balance": 0.00000014},
|
||||
{ "address": "QTW9TTM7fM4ghv1UAfa4L6w25D9PsKeh3f", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QTWc2J5jHUEeqyxSCQvnZu1GXuEWdFN8U3", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QTx98gU8ErigXkViWkvRH5JfaMpk8b3bHe", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTxK2iBYyj3Qwcfwo8vjtVvmLQmZVVME1D", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTy8J1dtbWc5KFBYJfLcoQAbzktV4JsxNp", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTyokTJrR4b2y76An3BFUEbqQy5vvg76iN", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTZEiy2RgGgyPpkMWE6trKYRHSGqPMufM5", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QTZh1TbhwyYWUdsMaTLVnWikotRBDvRwVz", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QU5sn5xsq5CwtzSQ8bqbgQkFR4CVUymtA5", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QU8vFmk1xUoRTFQuRupck4HSeeuYFAVMjw", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUaH6kB6Jk5mZfsFpdyKvYzFA12j4g2Bss", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QUarYYMPRodjEEBKrGsTsufPa1pc5M9mVk", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUAXAog3G9F8cJMC3KL4iGGFC3AK5hrzzP", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUCbBSPjDjygRJehHwjcXtM7PngUXKMiLW", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUCbFnjNwYfugM29oh6syCMnpr68vXuQjN", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QUdjqijDoyc83K4WcMW1sCn7zLd2t1WTqn", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUGo9SErgc6ceB5aBzcSJDNqBkQ9eaCKZS", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUgUMBsdauY9p7ahjkEkxPnH51vqZ6fEit", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QUhQUjdExXnbX6BYNSHNYohv8WUQgDpCYP", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUjcZYLfVsmJdB57w4rbvPKuSe26hoX8nA", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QUjm9fPRs4wbvXmwUYdMDg3HdNxGuR1DBo", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUJyCt8ZMDauaH4avg84gCbLY5Es2KJVFM", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUKKwug9PNai3DBggXUXP8Ag7WmR5SVUR4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUNYcKorTAjcFEFH2kLuGzTHDSXHbTm9n4", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QUNYcKorTAjcFEFH2kLuGzTHDSXHbTm9n4", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QUo3i1Ae9apv8muRUZuKaz7oTbRdKDWKgd", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUon9BuHPfvwS74tju9apvSioPGRh2R9f2", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUQdkr2SVCBcDTXVseG7MuZshQxSwyGZB2", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QURLLepAaEaUQuQKT3PQ1zMMTD4w8ztuxD", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUt4pPZnFH3Sd1NhQNg5CEbKhGZHceTqNb", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUTM1cfWdFFehQx2MdNENSqZKh1aqR4Z7K", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUvoLFfkuVuRe1KGMLQS4nUHry6CBTuTYz", "assetId": 0, "balance": 0.00000004},
|
||||
{ "address": "QUvtYEENi8wPXqCE2kereZaNxNgXrVivYr", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUw11tpoaCGqYvXdNoLE67vbTaRGvkAP8i", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUwdTXDoZ5BPMeW53e2epqV987jWej2Nk6", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QUXga5K8nzd9EqYtvEesZWEYuA688h6D3d", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUxh6PNsKhwJ12qGaM3AC1xZjwxy4hk1RG", "assetId": 0, "balance": 0.00000059},
|
||||
{ "address": "QUXqBSukt3Lmp8qBdCMtaM2P4qFGTBarCw", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUxxuGuZX141B6ZzDds6oojPHGqEM3cPNV", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QUYF2HhJF1tF4avi5xByPwwWhguHYXXLWL", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QUYfELYYXqHxEELSfDixQUL6ZqxvgqCtxE", "assetId": 0, "balance": -0.00001256},
|
||||
{ "address": "QUZCxNDBcv74PfrP9dXk1SbEsaQnKdb2Nd", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUZQPWhrxpze32vGiux6wa85kg9iwuhCDx", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QUzUCfoakDqBaL5zBgfvTKLHcuxbUfB38Q", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QUzUCfoakDqBaL5zBgfvTKLHcuxbUfB38Q", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QV2HChYd7opM1r6oYaX7KA5VUoKdiUuagg", "assetId": 0, "balance": 0.00000007},
|
||||
{ "address": "QV4496JU7VU9hwfZBwwprEGUv2d1RedQWz", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QV7bS2gnJnTzL38eD2YjNvBZFwQwe4Mw5U", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QVA3VtN9yYQDuptoTRCXoPDtvuwgW4pjH6", "assetId": 0, "balance": -0.00001256},
|
||||
{ "address": "QVaeUJbQHTamCcbULtSiiMFHsM2fqQunsy", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QVbDaDCrHEq8s5nfhHhFf3m712kAqbzFL5", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QVbSXYN5wdKL5u5QZnJiYQgY9BeTGmfs7z", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QVBWwms8Goc9ruUSPFtBt48b36uzQmKVdo", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QVchuzuoTKxNi2aMhype6sS7HCRLhdDrvw", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QVeSskDtxCQz7xj5GQcHrPgK5Kdtevjgc4", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QVgFYrrQV9feh45kAT6DyBonxdiJxvpmzh", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QVHWbjbpjg9zPXfyET7Sjb7JA9BBMWL9Qr", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QVi5jjTjJNoUg9kXSKAQPzxNA3yYsKBnEE", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QViKVZa3M3ar7RBRSBMTx8FdzLh1zxUhN8", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QViPTQGYNRXN7SQQEoNKvFnEW56X2sBqj8", "assetId": 0, "balance": -0.00006713},
|
||||
{ "address": "QVLuvt9krmxXwQPAeAhxzhuMF5i8F4aNs8", "assetId": 0, "balance": 0.00000015},
|
||||
{ "address": "QVLuvt9krmxXwQPAeAhxzhuMF5i8F4aNs8", "assetId": 2, "balance": 0.00000015},
|
||||
{ "address": "QVnHHnf5ZPBpbLZQabtjZBzi9TPgtqABqc", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QVpVKaXziXmP8qawtxqaFN8mHFAqvuiWzY", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QVrvy4ac2jBTfxyCKB7MLimqJooTDBApmS", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QVSo56b2nsrEbWzi3FBkGQCLJNyk9b9j7a", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QVSp1MvSTBut7shWdddfwdsWf9c7snBDwS", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QVSqUrNFR4mPTMa7UdVmNKZTSaDVAv8XXF", "assetId": 0, "balance": 0.00000015},
|
||||
{ "address": "QVSqUrNFR4mPTMa7UdVmNKZTSaDVAv8XXF", "assetId": 2, "balance": 0.00000015},
|
||||
{ "address": "QVuksgNt3QAr7KCrkxtE5FWrczfgLKxs4H", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QVurebcEbe4USR4xcS3Mbk12mhxsjRX31u", "assetId": 0, "balance": 0.00000052},
|
||||
{ "address": "QVurebcEbe4USR4xcS3Mbk12mhxsjRX31u", "assetId": 2, "balance": 0.00000048},
|
||||
{ "address": "QW1ip98ypmMmcSRjCRkS7Jd1SfneQbU7fq", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QWb8NhsKVEnfM8NSMPdSWSdn1T4zkCDDFD", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QWBFK5h61ZxGfqQpEkwwKTcLAo8t9VWe4K", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWBrxCkBSMNaL5ssPEawjfP9qUdurrFmP3", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWC7MydcEFhjENmCS2YABKY5F5BQd8XYyA", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWcgaTFfxt1cZL7hn9G8ayo81WT13S5ECM", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWcqcnuDeNpeUYqcvsJtXYdCpDb35ehAv6", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QWe1iPDudLU189BggPykbH1DrAeaFEgX6W", "assetId": 0, "balance": 0.00000059},
|
||||
{ "address": "QWe1iPDudLU189BggPykbH1DrAeaFEgX6W", "assetId": 2, "balance": 0.00000059},
|
||||
{ "address": "QWFHfqYNCYW9EN63zdprYSFQx2ApS6Hj2z", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWigG4GAT8eQ6rmNo4AdcGjF5ygSTAV1Q1", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QWinRb65f2g3yBoaZvTrQKQk7CW7vfBgGX", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWK94e5PzrBN5gHrFd77dHeP5XtCiWxVj5", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QWKYjxBUt2c6BHm26c4k7U8iF9eUEEAeQy", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWL7kZp6Pdd1bhxZ6SXPhVf5g7GParG9CC", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWLpsGYrkF2cy3tH6DCxso7kXZpZJvv13e", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QWMEPx9QfK4ErsHx4RwyoWL1cf4xqpBzXy", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QWmkcX9Ak4EJMZ6JZskF5uqBxiqK6R9c8s", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWN4qgyBfSn9TRTJM9e8ftzuAZmSuadmt5", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QWomBbcXNTdkyuPFUafwtBfpbxHzmUZzqi", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QWomBbcXNTdkyuPFUafwtBfpbxHzmUZzqi", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QWRpDYNycvqQrL9RmMDraL1hjTRBbghekz", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWS3EtVcBFz9iFjeANx1hTNN8Pfxi2Ft6H", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWuW2YMygVtWieUo6a4yayD1xFDWdnmo5j", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QWvTdm9LU1GSX9q6Rrvgx7xjo2iuV2Gxn1", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QWvTdm9LU1GSX9q6Rrvgx7xjo2iuV2Gxn1", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QWwBj6cFoM5EAE7sXULVw2BjVMCPTxDmVs", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWwcZuuDMpZtzFvjQthW6FUaUwpkzsmBN4", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWwrtjBL4ah965XPXHYJhymreC9jyryNLZ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QWYxUJmR6M6tvyxZASux3pxWuq1iWqTPei", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QWzjhwJg7u7EAfJVvRffiENs5ufhevZNso", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QX5g6nyYJrbiMxdcHdcHgbfA8jURQcEZGZ", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QX6TiCGH3oJKucGW2vEYU3kRKBuXSZLZtn", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QX8mxo977eANNG6Q59Z4dCW3eX3HPBrZ1R", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QX92FzQwtqm4svY7TR4gxt1aVAjEzdNnKo", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXapyoyeuUZ44m8PdJ2XcMdADkrMeAeRzF", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXaXcaaL1eDZQaECk47BCsjHojWGfHLcw2", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXB9jbqCrYBA68vgzrr8Z8bqMXrCvyU7Z1", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXdyEzgLMniSSzf2PS7hQhqUX6XKQemJnv", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QXeW3vzV8Rfe9kUbm15BW9dFFuXuBq8feb", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXFVYCWTAnM3FVhpYkin3Yu7WPb8w7NNZ2", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QXHEZ4axuNq91K5wW9zaNSvtLzsdsQ1yVz", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QXHMjGxDjd1RbNN4o2XdmXBdpiS6emQ8QL", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXHmtFXzf4D7PEu73NfBm3sZyeuGrm3QC5", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXjqVCQ8RaaC7T6Tyiag26Ruj1Tyrx9PvP", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXKmtkHHwaUQzGeHHG2dFiHUnKAp815Mzq", "assetId": 0, "balance": 0.00000026},
|
||||
{ "address": "QXKmtkHHwaUQzGeHHG2dFiHUnKAp815Mzq", "assetId": 2, "balance": 0.00000015},
|
||||
{ "address": "QXm5e16Lq6dnYwpZJ8Rn2cME3ziHZfRRnp", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QXmAdL5wEpgWbTSgnHJgdfQmKkhnx4EfaC", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QXmjUhJ7hmcQRrZ1UvnJAeFhp3aYiwL3zq", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXmYaDzKQdGiAMncJCr1FqXy6tX3avMRm9", "assetId": 0, "balance": 0.00003632},
|
||||
{ "address": "QXmYaDzKQdGiAMncJCr1FqXy6tX3avMRm9", "assetId": 2, "balance": 0.00000004},
|
||||
{ "address": "QXNzWKLR9pqHW5KCUCFvcaUTwWKWvdYhzi", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QXqc1jH2DL6H3qFCHxTBABivtkqJsoBYyQ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXrkYRBJkp3CQ2ryjvWskszuWTXRRLbhTB", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QXUWuZ2oAUodMU8EAQkAkDwkQHS1SFxpts", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QXUWuZ2oAUodMU8EAQkAkDwkQHS1SFxpts", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QXV2EabmW5AqDa4usWyv13QvxtkSUF5LFs", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QXXfBJz9UfAgTEAn3b9W9jxmJYtqar1P78", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXYk68x2tiUrDBv8eq6wd4KtBmLHYiC4zR", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXYY8BwyYn71nDg9UKKMveyTLPZWErrtaT", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QXZ3Uqs3KcfZDFgCURso8upzmPxxHtD9rT", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QXzpeNdKwoxycyqZ2UanqFGngCN72nYygj", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QY1RFZTD2ogRohf3UrdT4g1Qo9D122AZDN", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QY1rJEuFJ5C6vp6QPczQbwUpg2F8KdPRoE", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QY4xWXWbyU4t2zrcpZUTAR3kcXpcXw63Qn", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QY6ZGZdi8h5op2VrRXkG1W5Jp3feLwp7ZD", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QY82MasqEH6ChwXaETH4piMtE8Pk4NBWD3", "assetId": 0, "balance": 0.00000103},
|
||||
{ "address": "QYA1jbLKSSY1q1Zo2VwuDe6vTJXQrr1wu1", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QYAbYY1mVfCcXSoWPmVzUhvh7UBaK5enER", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYAYB8m9CfksGvEjGnj49q74bNDCGGaZqV", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYB9dJizBvcsmhFBgB4tzLAKsQvb5HQggo", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYcEBpuJ9RmdFGX6cdKAjSwnNVhwbtFLdr", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYD3kXchZ86vUyJBXNCVQ4LUvTAd6PUZW3", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYGcPZcRhGaY1MsiDr3VtwTXmB9TAbLFSn", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QYGNMWBmqWgVtMWGHypAsKhDVQw5mrFZww", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QYGrsQT4yhRUxiKfVgo8M5Sovfy1zcjUsr", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYgVi26jUqMzJo4ahZV9yekQNnYKHBaX8r", "assetId": 0, "balance": 0.00000132},
|
||||
{ "address": "QYgVi26jUqMzJo4ahZV9yekQNnYKHBaX8r", "assetId": 2, "balance": 0.00000092},
|
||||
{ "address": "QYHvrW3bwYFeMTUEYascXhXkBAzUkcGbqn", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYicTvqqPFt7buJfRd9cgs2xvJ2rnvyTzX", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYkpsUvut4eufXqJUnUbCzDajK5RyQ1Vzg", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYn2Uh4eii4SE29BpEPeRySbAeb9R6tGbf", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYoNKJhgva9ECexhgAmB3r4ucM8xwJbTWu", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYp5W4kGvCHfzeCgDyoCAWBZ9gViECNS5J", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYphDYA1te9acFNc7FEmFBu3FTTomp4ATZ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYppKiFc7zt1EDXy1dUwHMHsnm2ckVsHTc", "assetId": 0, "balance": 0.00002352},
|
||||
{ "address": "QYREQw3ohthywupqzLBRMjGkRSvbFPLBow", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QYsh2NB6TogqV1iXHmHXcVaWw25WEYA94o", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QYsh2NB6TogqV1iXHmHXcVaWw25WEYA94o", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QYt1n7g68RrttdF8BdnqZkkKcYq1RHTeBF", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QYTDS3XqzHWcqmhXTsDcUDVAbQVXXVaVVs", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QYTmTSxB8GdnruZWA7Dvod9ihRQrAiLxn1", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QYZj8LFUwQVKZGBLe9FTgWnf6pLEyJJDZi", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZ19JRpSsgvm4z6EjnbhdxJBoUYzDGvP3x", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZ1iWoraqiezeAHrgTsC13MTcrwHJdRwgk", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QZ2gi6BhUNpGmrErgJLFuY1WHy6xK1J7qX", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZ7wvWAUcHKRhvQ3ijdrqM4zucQKCgQ1hQ", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZb1jPdakvcB9f7aVRU3wLXRcixgk8tPdU", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZb81oH9N6M4ZjPstDJuceARrdjLi8dY1x", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZbBJuoU892QYTQ4N1sJT9bVE3HNNdSw55", "assetId": 0, "balance": -0.00003608},
|
||||
{ "address": "QZBTByprtp1MGQbEND6H95cPsrGaKEJEmy", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QZCauGWyChXBgEQiXAJLmSaaz94Asgi8wU", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZgpMDQeZ3ReC13wnTvP94hVJoyAgVXEs6", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZHC8bBNbHTSEmdjKMQJFHAJhRwrTBXje1", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZiMCvxMJqG3bG6SsET43zegm4mtm2TABA", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZiYh4m4Uh3FH52cnow8MrNyXhSH88bp2H", "assetId": 0, "balance": 0.00003639},
|
||||
{ "address": "QZiYh4m4Uh3FH52cnow8MrNyXhSH88bp2H", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QZJc1V32oFm8tufB4bk7fa3aepu4EdkeDU", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZjCgcSVvSRsFZeLJz9C5dTa36s3cSKqvB", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QZKKSYCnTaB56dT1dkXiV86eU6Pc9ADos2", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZkThgFfExognAbxLjYZGVHpL7X6g3EG4A", "assetId": 0, "balance": 0.00000026},
|
||||
{ "address": "QZkThgFfExognAbxLjYZGVHpL7X6g3EG4A", "assetId": 2, "balance": 0.00000015},
|
||||
{ "address": "QZMhBGpATjZ9ZK3fdcRvXW3RWKAAETymQa", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QZMzF4iTBV93LP5Vkv7Ka3Q2xjdUwcUhcV", "assetId": 0, "balance": 0.00000103},
|
||||
{ "address": "QZNDNgaBJhkUjtb66hGvjAs3s1V2TESDxE", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZpCNquYLc5B6xiUwsPtMB7M6f1CWcLBwP", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZqfJg1raAA3AzuivGD6sCQfQQcekAM6tx", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZRBPwvFBv59rZ4MzuPnjVi7cq5Uv7WqqR", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZtebdFopCUyGBQs5S5WYPckPcVZh19E4q", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QZvHW7amu5DNktsBgaMrR1brHZhhhVwKLW", "assetId": 0, "balance": 0.00000011},
|
||||
{ "address": "QZw7tgMttSySNMKfcMrEbdtnqHVrQ9w9fT", "assetId": 0, "balance": 0.00003639},
|
||||
{ "address": "QZw7tgMttSySNMKfcMrEbdtnqHVrQ9w9fT", "assetId": 2, "balance": 0.00000011},
|
||||
{ "address": "QZWL5atv3jQi3SdcQPS91vGhbk4Mi5CF8z", "assetId": 0, "balance": 0.00001276},
|
||||
{ "address": "QZxNth97o4UNw6XbDY7fnykzuKaxmmqaR1", "assetId": 0, "balance": 0.00003628},
|
||||
{ "address": "QZXo75Sk5AHuuuRX4VcHCBvcHaCHGHBVa2", "assetId": 0, "balance": -0.00010321},
|
||||
{ "address": "QZzxwbQZ7Gi4kSVa39bcXb3q12AhGRQDXA", "assetId": 0, "balance": -0.00010321}
|
||||
]
|
||||
@@ -1,13 +1,13 @@
|
||||
|
||||
INVALID_ADDRESS = ung\u00FCltige adresse
|
||||
INVALID_ADDRESS = ungültige adresse
|
||||
|
||||
INVALID_ASSET_ID = ung\u00FCltige asset ID
|
||||
INVALID_ASSET_ID = ungültige asset ID
|
||||
|
||||
INVALID_DATA = ung\u00FCltige daten
|
||||
INVALID_DATA = ungültige daten
|
||||
|
||||
INVALID_PUBLIC_KEY = ung\u00FCltiger public key
|
||||
INVALID_PUBLIC_KEY = ungültiger public key
|
||||
|
||||
INVALID_SIGNATURE = ung\u00FCltige signatur
|
||||
INVALID_SIGNATURE = ungültige signatur
|
||||
|
||||
JSON = JSON nachricht konnte nicht geparsed werden
|
||||
|
||||
|
||||
@@ -1,53 +1,57 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# Keys are from api.ApiError enum
|
||||
|
||||
ADDRESS_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0083\u00D1\u0087\u00D0\u00B5\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D1\u008C
|
||||
|
||||
# Blocks
|
||||
BLOCK_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA
|
||||
|
||||
CANNOT_MINT = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D1\u0087\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u0082\u00D1\u008C
|
||||
|
||||
GROUP_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0
|
||||
|
||||
INVALID_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
|
||||
|
||||
# Assets
|
||||
INVALID_ASSET_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
||||
|
||||
INVALID_CRITERIA = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00BA\u00D1\u0080\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D1\u0080\u00D0\u00B8\u00D0\u00B8 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B8\u00D1\u0081\u00D0\u00BA\u00D0\u00B0
|
||||
|
||||
INVALID_DATA = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00B4\u00D0\u00B0\u00D0\u00BD\u00D0\u00BD\u00D1\u008B\u00D0\u00B5
|
||||
|
||||
INVALID_HEIGHT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B2\u00D1\u008B\u00D1\u0081\u00D0\u00BE\u00D1\u0082\u00D0\u00B0 \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA\u00D0\u00B0
|
||||
|
||||
INVALID_NETWORK_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D1\u0081\u00D0\u00B5\u00D1\u0082\u00D0\u00B5\u00D0\u00B2\u00D0\u00BE\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
|
||||
|
||||
INVALID_ORDER_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
||||
|
||||
INVALID_PRIVATE_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
|
||||
|
||||
INVALID_PUBLIC_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
|
||||
|
||||
INVALID_REFERENCE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0081\u00D1\u008B\u00D0\u00BB\u00D0\u00BA\u00D0\u00B0
|
||||
|
||||
# Validation
|
||||
INVALID_SIGNATURE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D1\u008C
|
||||
|
||||
JSON = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0083\u00D0\u00B4\u00D0\u00B0\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D1\u008C \u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D1\u0081\u00D0\u00BE\u00D0\u00BE\u00D0\u00B1\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 json
|
||||
|
||||
NAME_UNKNOWN = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00BE
|
||||
|
||||
ORDER_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
||||
|
||||
PUBLIC_KEY_NOT_FOUND = \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B0\u00D0\u00B9\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD
|
||||
|
||||
REPOSITORY_ISSUE = \u00D0\u00BE\u00D1\u0088\u00D0\u00B8\u00D0\u00B1\u00D0\u00BA\u00D0\u00B0 \u00D1\u0080\u00D0\u00B5\u00D0\u00BF\u00D0\u00BE\u00D0\u00B7\u00D0\u00B8\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
TRANSACTION_INVALID = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0: %s (%s)
|
||||
|
||||
TRANSACTION_UNKNOWN = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0
|
||||
|
||||
TRANSFORMATION_ERROR = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0083\u00D0\u00B4\u00D0\u00B0\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D1\u008C \u00D0\u00BF\u00D1\u0080\u00D0\u00B5\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D1\u008C JSON \u00D0\u00B2 \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008E
|
||||
|
||||
UNAUTHORIZED = \u00D0\u00B2\u00D1\u008B\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2 API \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B2\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B8\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BD
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# Keys are from api.ApiError enum
|
||||
|
||||
ADDRESS_UNKNOWN = неизвестная учетная запись
|
||||
|
||||
BLOCKCHAIN_NEEDS_SYNC = блокчейн должен сначала синхронизироваться
|
||||
|
||||
# Blocks
|
||||
BLOCK_UNKNOWN = неизвестный блок
|
||||
|
||||
CANNOT_MINT = аккаунт не может чеканить
|
||||
|
||||
GROUP_UNKNOWN = неизвестная группа
|
||||
|
||||
INVALID_ADDRESS = неизвестный адрес
|
||||
|
||||
# Assets
|
||||
INVALID_ASSET_ID = неверный идентификатор актива
|
||||
|
||||
INVALID_CRITERIA = неверные критерии поиска
|
||||
|
||||
INVALID_DATA = неверные данные
|
||||
|
||||
INVALID_HEIGHT = недопустимая высота блока
|
||||
|
||||
INVALID_NETWORK_ADDRESS = неверный сетевой адрес
|
||||
|
||||
INVALID_ORDER_ID = неверный идентификатор заказа актива
|
||||
|
||||
INVALID_PRIVATE_KEY = неверный приватный ключ
|
||||
|
||||
INVALID_PUBLIC_KEY = недействительный открытый ключ
|
||||
|
||||
INVALID_REFERENCE = неверная ссылка
|
||||
|
||||
# Validation
|
||||
INVALID_SIGNATURE = недействительная подпись
|
||||
|
||||
JSON = не удалось разобрать сообщение json
|
||||
|
||||
NAME_UNKNOWN = имя неизвестно
|
||||
|
||||
NON_PRODUCTION = этот вызов API не разрешен для производственных систем
|
||||
|
||||
ORDER_UNKNOWN = неизвестный идентификатор заказа актива
|
||||
|
||||
PUBLIC_KEY_NOT_FOUND = открытый ключ не найден
|
||||
|
||||
REPOSITORY_ISSUE = ошибка репозитория
|
||||
|
||||
TRANSACTION_INVALID = транзакция недействительна: %s (%s)
|
||||
|
||||
TRANSACTION_UNKNOWN = транзакция неизвестна
|
||||
|
||||
TRANSFORMATION_ERROR = не удалось преобразовать JSON в транзакцию
|
||||
|
||||
UNAUTHORIZED = вызов API не авторизован
|
||||
|
||||
@@ -19,6 +19,8 @@ CREATING_BACKUP_OF_DB_FILES = Creating backup of database files...
|
||||
|
||||
DB_BACKUP = Database Backup
|
||||
|
||||
DB_CHECKPOINT = Database Checkpoint
|
||||
|
||||
EXIT = Exit
|
||||
|
||||
MINTING_DISABLED = NOT minting
|
||||
@@ -34,6 +36,8 @@ NTP_NAG_TEXT_WINDOWS = Select "Synchronize clock" from menu to fix.
|
||||
|
||||
OPEN_UI = Open UI
|
||||
|
||||
PERFORMING_DB_CHECKPOINT = Saving uncommitted database changes...
|
||||
|
||||
SYNCHRONIZE_CLOCK = Synchronize clock
|
||||
|
||||
SYNCHRONIZING_BLOCKCHAIN = Synchronizing
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# SysTray pop-up menu
|
||||
|
||||
BLOCK_HEIGHT = \u5757\u9AD8\u5EA6
|
||||
BLOCK_HEIGHT = 块高度
|
||||
|
||||
CHECK_TIME_ACCURACY = \u68C0\u67E5\u65F6\u95F4\u51C6\u786E\u6027
|
||||
CHECK_TIME_ACCURACY = 检查时间准确性
|
||||
|
||||
CONNECTION = \u4E2A\u8FDE\u63A5
|
||||
CONNECTION = 个连接
|
||||
|
||||
CONNECTIONS = \u4E2A\u8FDE\u63A5
|
||||
CONNECTIONS = 个连接
|
||||
|
||||
EXIT = \u9000\u51FA\u8F6F\u4EF6
|
||||
EXIT = 退出软件
|
||||
|
||||
MINTING_DISABLED = \u6CA1\u6709\u94F8\u5E01
|
||||
MINTING_DISABLED = 没有铸币
|
||||
|
||||
MINTING_ENABLED = \u2714 \u94F8\u5E01
|
||||
MINTING_ENABLED = ✔ 铸币
|
||||
|
||||
# Nagging about lack of NTP time sync
|
||||
NTP_NAG_CAPTION = \u7535\u8111\u7684\u65F6\u949F\u4E0D\u51C6\u786E\uFF01
|
||||
NTP_NAG_CAPTION = 电脑的时钟不准确!
|
||||
|
||||
NTP_NAG_TEXT_UNIX = \u5B89\u88C5NTP\u670D\u52A1\u4EE5\u83B7\u5F97\u51C6\u786E\u7684\u65F6\u949F\u3002
|
||||
NTP_NAG_TEXT_UNIX = 安装NTP服务以获得准确的时钟。
|
||||
|
||||
NTP_NAG_TEXT_WINDOWS = \u4ECE\u83DC\u5355\u4E2D\u9009\u62E9\u201C\u540C\u6B65\u65F6\u949F\u201D\u8FDB\u884C\u4FEE\u590D\u3002
|
||||
NTP_NAG_TEXT_WINDOWS = 从菜单中选择“同步时钟”进行修复。
|
||||
|
||||
OPEN_UI = \u5F00\u542F\u754C\u9762
|
||||
OPEN_UI = 开启界面
|
||||
|
||||
SYNCHRONIZE_CLOCK = \u540C\u6B65\u65F6\u949F
|
||||
SYNCHRONIZE_CLOCK = 同步时钟
|
||||
|
||||
SYNCHRONIZING_BLOCKCHAIN = \u540C\u6B65\u533A\u5757\u94FE
|
||||
SYNCHRONIZING_BLOCKCHAIN = 同步区块链
|
||||
|
||||
SYNCHRONIZING_CLOCK = \u540C\u6B65\u7740\u65F6\u949F
|
||||
SYNCHRONIZING_CLOCK = 同步着时钟
|
||||
|
||||
@@ -1,164 +1,176 @@
|
||||
|
||||
ACCOUNT_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
ACCOUNT_CANNOT_REWARD_SHARE = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B8\u00D1\u0082\u00D1\u008C\u00D1\u0081\u00D1\u008F \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5\u00D0\u00BC
|
||||
|
||||
ALREADY_GROUP_ADMIN = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B4\u00D0\u00BC\u00D0\u00B8\u00D0\u00BD\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
ALREADY_GROUP_MEMBER = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0087\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
ALREADY_VOTED_FOR_THAT_OPTION = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B3\u00D0\u00BE\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BB\u00D0\u00B8 \u00D0\u00B7\u00D0\u00B0 \u00D1\u008D\u00D1\u0082\u00D0\u00BE\u00D1\u0082 \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082
|
||||
|
||||
ASSET_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
ASSET_DOES_NOT_EXIST = \u00D0\u0090\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
ASSET_DOES_NOT_MATCH_AT = \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00B2\u00D0\u00BF\u00D0\u00B0\u00D0\u00B4\u00D0\u00B0\u00D0\u00B5\u00D1\u0082 \u00D1\u0081 \u00D0\u0090\u00D0\u00A2
|
||||
|
||||
AT_ALREADY_EXISTS = AT \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
AT_IS_FINISHED = AT \u00D0\u00B2 \u00D0\u00B7\u00D0\u00B0\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D1\u0088\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B8
|
||||
|
||||
AT_UNKNOWN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u0090\u00D0\u00A2
|
||||
|
||||
BANNED_FROM_GROUP = \u00D0\u00B8\u00D1\u0081\u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087\u00D0\u00B5\u00D0\u00BD \u00D0\u00B8\u00D0\u00B7 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
BAN_EXISTS = \u00D0\u0091\u00D0\u00B0\u00D0\u00BD
|
||||
|
||||
BAN_UNKNOWN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BD
|
||||
|
||||
BUYER_ALREADY_OWNER = \u00D0\u00BF\u00D0\u00BE\u00D0\u00BA\u00D1\u0083\u00D0\u00BF\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00B1\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D0\u00BD\u00D0\u00BD\u00D0\u00B8\u00D0\u00BA
|
||||
|
||||
DUPLICATE_OPTION = \u00D0\u00B4\u00D1\u0083\u00D0\u00B1\u00D0\u00BB\u00D0\u00B8\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082
|
||||
|
||||
GROUP_ALREADY_EXISTS = \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
GROUP_APPROVAL_DECIDED = \u00D0\u00B3\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D0\u00BE\u00D0\u00B4\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
|
||||
|
||||
GROUP_APPROVAL_NOT_REQUIRED = \u00D0\u00B3\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00BE\u00D0\u00B2\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BE\u00D0\u00B4\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0082\u00D1\u0080\u00D0\u00B5\u00D0\u00B1\u00D1\u0083\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u008F
|
||||
|
||||
GROUP_DOES_NOT_EXIST = \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
GROUP_ID_MISMATCH = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00BE\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B0 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
GROUP_OWNER_CANNOT_LEAVE = \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D1\u0083\u00D0\u00B9\u00D1\u0082\u00D0\u00B8
|
||||
|
||||
HAVE_EQUALS_WANT = \u00D0\u00B8\u00D0\u00BC\u00D0\u00BC\u00D0\u00B5\u00D1\u008E\u00D1\u0082\u00D1\u0081\u00D1\u008F \u00D1\u0080\u00D0\u00B0\u00D0\u00B2\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00B6\u00D0\u00B5\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
INSUFFICIENT_FEE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BF\u00D0\u00BB\u00D0\u00B0\u00D1\u0082\u00D0\u00B0
|
||||
|
||||
INVALID_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
|
||||
|
||||
INVALID_AMOUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D0\u00BC\u00D0\u00BC\u00D0\u00B0
|
||||
|
||||
INVALID_ASSET_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
|
||||
|
||||
INVALID_AT_TRANSACTION = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u0090\u00D0\u00A2 \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
INVALID_AT_TYPE_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00BE \u00D0\u00B4\u00D0\u00BB\u00D1\u008F \u00D1\u0082\u00D0\u00B8\u00D0\u00BF\u00D0\u00B0 \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D1\u008B AT
|
||||
|
||||
INVALID_CREATION_BYTES = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B5 \u00D0\u00B1\u00D0\u00B0\u00D0\u00B9\u00D1\u0082\u00D1\u008B \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
INVALID_DESCRIPTION_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00BE\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
INVALID_GROUP_APPROVAL_THRESHOLD = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D0\u00BE\u00D1\u0080\u00D0\u00BE\u00D0\u00B3 \u00D1\u0083\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
INVALID_GROUP_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
INVALID_GROUP_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083 \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
INVALID_LIFETIME = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083 \u00D1\u0081\u00D1\u0080\u00D0\u00BE\u00D0\u00BA \u00D1\u0081\u00D0\u00BB\u00D1\u0083\u00D0\u00B6\u00D0\u00B1\u00D1\u008B
|
||||
|
||||
INVALID_NAME_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
INVALID_NAME_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D1\u0086\u00D0\u00B0
|
||||
|
||||
INVALID_OPTIONS_COUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00BE \u00D0\u00BE\u00D0\u00BF\u00D1\u0086\u00D0\u00B8\u00D0\u00B9
|
||||
|
||||
INVALID_OPTION_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00BE\u00D0\u00BF\u00D1\u0086\u00D0\u00B8\u00D0\u00B8
|
||||
|
||||
INVALID_ORDER_CREATOR = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0
|
||||
|
||||
INVALID_PAYMENTS_COUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D1\u0081\u00D1\u0087\u00D0\u00B5\u00D1\u0082 \u00D0\u00BF\u00D0\u00BB\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00B6\u00D0\u00B5\u00D0\u00B9
|
||||
|
||||
INVALID_PUBLIC_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
|
||||
|
||||
INVALID_QUANTITY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00BE
|
||||
|
||||
INVALID_REFERENCE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0081\u00D1\u008B\u00D0\u00BB\u00D0\u00BA\u00D0\u00B0
|
||||
|
||||
INVALID_RETURN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00B2\u00D1\u0080\u00D0\u00B0\u00D1\u0082
|
||||
|
||||
INVALID_REWARD_SHARE_PERCENT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0086\u00D0\u00B5\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
INVALID_SELLER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B2\u00D0\u00B5\u00D1\u0086
|
||||
|
||||
INVALID_TAGS_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D1\u0082\u00D1\u008D\u00D0\u00B3\u00D0\u00BE\u00D0\u00B2
|
||||
|
||||
INVALID_TX_GROUP_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B \u00D0\u00BF\u00D0\u00B5\u00D1\u0080\u00D0\u00B5\u00D0\u00B4\u00D0\u00B0\u00D1\u0087\u00D0\u00B8
|
||||
|
||||
INVALID_VALUE_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D1\u0087\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D1\u008B
|
||||
|
||||
JOIN_REQUEST_EXISTS = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081 \u00D0\u00BD\u00D0\u00B0 \u00D0\u00BF\u00D1\u0080\u00D0\u00B8\u00D1\u0081\u00D0\u00BE\u00D0\u00B5\u00D0\u00B4\u00D0\u00B8\u00D0\u00BD\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
MAXIMUM_REWARD_SHARES = \u00D0\u00BC\u00D0\u00B0\u00D0\u00BA\u00D1\u0081\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5
|
||||
|
||||
MISSING_CREATOR = \u00D0\u00BE\u00D1\u0082\u00D1\u0081\u00D1\u0083\u00D1\u0082\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D1\u008E\u00D1\u0089\u00D0\u00B8\u00D0\u00B9 \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C
|
||||
|
||||
MULTIPLE_NAMES_FORBIDDEN = \u00D0\u00BD\u00D0\u00B5\u00D1\u0081\u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D1\u008C\u00D0\u00BA\u00D0\u00BE \u00D0\u00B8\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0080\u00D0\u00B5\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00BE
|
||||
|
||||
NAME_ALREADY_FOR_SALE = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B2 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B6\u00D0\u00B5
|
||||
|
||||
NAME_ALREADY_REGISTERED = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B7\u00D0\u00B0\u00D1\u0080\u00D0\u00B5\u00D0\u00B3\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B8\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BD\u00D0\u00BE
|
||||
|
||||
NAME_DOES_NOT_EXIST = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
NAME_NOT_FOR_SALE = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u008F
|
||||
|
||||
NAME_NOT_LOWER_CASE = \u00D0\u00B8\u00D0\u00BC\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B4\u00D0\u00BE\u00D0\u00BB\u00D0\u00B6\u00D0\u00BD\u00D0\u00BE \u00D1\u0081\u00D0\u00BE\u00D0\u00B4\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D1\u0080\u00D0\u00B5\u00D0\u00B3\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080
|
||||
|
||||
NEGATIVE_AMOUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D0\u00BC\u00D0\u00BC\u00D0\u00B0
|
||||
|
||||
NEGATIVE_FEE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BA\u00D0\u00BE\u00D0\u00BC\u00D0\u00B8\u00D1\u0081\u00D1\u0081\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
NEGATIVE_PRICE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0082\u00D0\u00BE\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D1\u008C
|
||||
|
||||
NOT_GROUP_ADMIN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B4\u00D0\u00BC\u00D0\u00B8\u00D0\u00BD\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
NOT_GROUP_MEMBER = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0087\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
|
||||
|
||||
NOT_MINTING_ACCOUNT = \u00D1\u0081\u00D1\u0087\u00D0\u00B5\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0087\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u0082
|
||||
|
||||
NOT_YET_RELEASED = \u00D0\u00B5\u00D1\u0089\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B2\u00D1\u008B\u00D0\u00BF\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00BE
|
||||
|
||||
NO_BALANCE = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D1\u0081\u00D0\u00B0
|
||||
|
||||
NO_BLOCKCHAIN_LOCK = \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA\u00D1\u0087\u00D0\u00B5\u00D0\u00B9\u00D0\u00BD \u00D1\u0083\u00D0\u00B7\u00D0\u00BB\u00D0\u00B0 \u00D0\u00B2 \u00D0\u00BD\u00D0\u00B0\u00D1\u0081\u00D1\u0082\u00D0\u00BE\u00D1\u008F\u00D1\u0089\u00D0\u00B5\u00D0\u00B5 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D1\u008F \u00D0\u00B7\u00D0\u00B0\u00D0\u00BD\u00D1\u008F\u00D1\u0082
|
||||
|
||||
NO_FLAG_PERMISSION = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D1\u0080\u00D0\u00B5\u00D1\u0088\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B0 \u00D1\u0084\u00D0\u00BB\u00D0\u00B0\u00D0\u00B3
|
||||
|
||||
OK = OK
|
||||
|
||||
ORDER_ALREADY_CLOSED = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082
|
||||
|
||||
ORDER_DOES_NOT_EXIST = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
POLL_ALREADY_EXISTS = \u00D0\u00BE\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
POLL_DOES_NOT_EXIST = \u00D0\u00BE\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
POLL_OPTION_DOES_NOT_EXIST = \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082\u00D0\u00BE\u00D0\u00B2 \u00D0\u00BE\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0082\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
PUBLIC_KEY_UNKNOWN = \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087 \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B5\u00D0\u00BD
|
||||
|
||||
SELF_SHARE_EXISTS = \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B8\u00D1\u0082\u00D1\u008C\u00D1\u0081\u00D1\u008F \u00D0\u00B4\u00D0\u00BE\u00D0\u00BB\u00D0\u00B5\u00D0\u00B9
|
||||
|
||||
TIMESTAMP_TOO_NEW = \u00D0\u00BD\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u008F \u00D0\u00BC\u00D0\u00B5\u00D1\u0082\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
|
||||
|
||||
TIMESTAMP_TOO_OLD = \u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0080\u00D0\u00B0\u00D1\u008F \u00D0\u00BC\u00D0\u00B5\u00D1\u0082\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
|
||||
|
||||
TRANSACTION_ALREADY_CONFIRMED = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
|
||||
|
||||
TRANSACTION_ALREADY_EXISTS = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
|
||||
|
||||
TRANSACTION_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F
|
||||
|
||||
ACCOUNT_ALREADY_EXISTS = аккаунт уже существует
|
||||
|
||||
ACCOUNT_CANNOT_REWARD_SHARE = аккаунт не может делиться вознаграждением
|
||||
|
||||
ALREADY_GROUP_ADMIN = уже администратор группы
|
||||
|
||||
ALREADY_GROUP_MEMBER = уже член группы
|
||||
|
||||
ALREADY_VOTED_FOR_THAT_OPTION = уже проголосовали за этот вариант
|
||||
|
||||
ASSET_ALREADY_EXISTS = актив уже существует
|
||||
|
||||
ASSET_DOES_NOT_EXIST = Актив не существует
|
||||
|
||||
ASSET_DOES_NOT_MATCH_AT = актив не совпадает с АТ
|
||||
|
||||
ASSET_NOT_SPENDABLE = актив не подлежит расходованию
|
||||
|
||||
AT_ALREADY_EXISTS = AT уже существует
|
||||
|
||||
AT_IS_FINISHED = AT в завершении
|
||||
|
||||
AT_UNKNOWN = не известный АТ
|
||||
|
||||
BANNED_FROM_GROUP = исключен из группы
|
||||
|
||||
BAN_EXISTS = Бан
|
||||
|
||||
BAN_UNKNOWN = не известный бан
|
||||
|
||||
BUYER_ALREADY_OWNER = покупатель уже собственник
|
||||
|
||||
CLOCK_NOT_SYNCED = часы не синхронизированы
|
||||
|
||||
DUPLICATE_OPTION = дублировать вариант
|
||||
|
||||
GROUP_ALREADY_EXISTS = группа уже существует
|
||||
|
||||
GROUP_APPROVAL_DECIDED = гуппа одобрена
|
||||
|
||||
GROUP_APPROVAL_NOT_REQUIRED = гупповое одобрение не требуется
|
||||
|
||||
GROUP_DOES_NOT_EXIST = группа не существует
|
||||
|
||||
GROUP_ID_MISMATCH = не соответствие идентификатора группы
|
||||
|
||||
GROUP_OWNER_CANNOT_LEAVE = владелец группы не может уйти
|
||||
|
||||
HAVE_EQUALS_WANT = иммеются равные желания
|
||||
|
||||
INSUFFICIENT_FEE = недостаточная плата
|
||||
|
||||
INVALID_ADDRESS = недействительный адрес
|
||||
|
||||
INVALID_AMOUNT = недопустимая сумма
|
||||
|
||||
INVALID_ASSET_OWNER = недействительный владелец актива
|
||||
|
||||
INVALID_AT_TRANSACTION = недействительная АТ транзакция
|
||||
|
||||
INVALID_AT_TYPE_LENGTH = недействительно для типа длины AT
|
||||
|
||||
INVALID_CREATION_BYTES = недопустимые байты создания
|
||||
|
||||
INVALID_DATA_LENGTH = недопустимая длина данных
|
||||
|
||||
INVALID_DESCRIPTION_LENGTH = недопустимая длина описания
|
||||
|
||||
INVALID_GROUP_APPROVAL_THRESHOLD = недопустимый порог утверждения группы
|
||||
|
||||
INVALID_GROUP_ID = недопустимый идентификатор группы
|
||||
|
||||
INVALID_GROUP_OWNER = недопу владелец группы
|
||||
|
||||
INVALID_LIFETIME = недопу срок службы
|
||||
|
||||
INVALID_NAME_LENGTH = недопустимая длина группы
|
||||
|
||||
INVALID_NAME_OWNER = недопустимое имя владельца
|
||||
|
||||
INVALID_OPTIONS_COUNT = неверное количество опций
|
||||
|
||||
INVALID_OPTION_LENGTH = недопустимая длина опции
|
||||
|
||||
INVALID_ORDER_CREATOR = недопустимый создатель заказа
|
||||
|
||||
INVALID_PAYMENTS_COUNT = неверный подсчет платежей
|
||||
|
||||
INVALID_PUBLIC_KEY = недействительный открытый ключ
|
||||
|
||||
INVALID_QUANTITY = недопустимое количество
|
||||
|
||||
INVALID_REFERENCE = неверная ссылка
|
||||
|
||||
INVALID_RETURN = недопустимый возврат
|
||||
|
||||
INVALID_REWARD_SHARE_PERCENT = недействительный процент награждения
|
||||
|
||||
INVALID_SELLER = недействительный продавец
|
||||
|
||||
INVALID_TAGS_LENGTH = недействительная длина тэгов
|
||||
|
||||
INVALID_TX_GROUP_ID = недействительный идентификатор группы передачи
|
||||
|
||||
INVALID_VALUE_LENGTH = недопустимое значение длины
|
||||
|
||||
INVITE_UNKNOWN = приглашать неизветсных
|
||||
|
||||
JOIN_REQUEST_EXISTS = запрос на присоединение существует
|
||||
|
||||
MAXIMUM_REWARD_SHARES = максимальное вознаграждение
|
||||
|
||||
MISSING_CREATOR = отсутствующий создатель
|
||||
|
||||
MULTIPLE_NAMES_FORBIDDEN = несколько имен запрещено
|
||||
|
||||
NAME_ALREADY_FOR_SALE = имя уже в продаже
|
||||
|
||||
NAME_ALREADY_REGISTERED = имя уже зарегистрировано
|
||||
|
||||
NAME_DOES_NOT_EXIST = имя не существует
|
||||
|
||||
NAME_NOT_FOR_SALE = имя не продается
|
||||
|
||||
NAME_NOT_LOWER_CASE = иммя не должно содержать строчный регистр
|
||||
|
||||
NEGATIVE_AMOUNT = недостаточная сумма
|
||||
|
||||
NEGATIVE_FEE = недостаточная комиссия
|
||||
|
||||
NEGATIVE_PRICE = недостаточная стоимость
|
||||
|
||||
NOT_GROUP_ADMIN = не администратор группы
|
||||
|
||||
NOT_GROUP_MEMBER = не член группы
|
||||
|
||||
NOT_MINTING_ACCOUNT = счет не чеканит
|
||||
|
||||
NOT_YET_RELEASED = еще не выпущено
|
||||
|
||||
NO_BALANCE = нет баланса
|
||||
|
||||
NO_BLOCKCHAIN_LOCK = блокчейн узла в настоящее время занят
|
||||
|
||||
NO_FLAG_PERMISSION = нет разрешения на флаг
|
||||
|
||||
OK = OK
|
||||
|
||||
ORDER_ALREADY_CLOSED = заказ закрыт
|
||||
|
||||
ORDER_DOES_NOT_EXIST = заказа не существует
|
||||
|
||||
POLL_ALREADY_EXISTS = опрос уже существует
|
||||
|
||||
POLL_DOES_NOT_EXIST = опроса не существует
|
||||
|
||||
POLL_OPTION_DOES_NOT_EXIST = вариантов ответа не существует
|
||||
|
||||
PUBLIC_KEY_UNKNOWN = открытый ключ неизвестен
|
||||
|
||||
SELF_SHARE_EXISTS = поделиться долей
|
||||
|
||||
TIMESTAMP_TOO_NEW = новая метка времени
|
||||
|
||||
TIMESTAMP_TOO_OLD = старая метка времени
|
||||
|
||||
TOO_MANY_UNCONFIRMED = много не подтвержденных
|
||||
|
||||
TRANSACTION_ALREADY_CONFIRMED = транзакция уже подтверждена
|
||||
|
||||
TRANSACTION_ALREADY_EXISTS = транзакция существует
|
||||
|
||||
TRANSACTION_UNKNOWN = неизвестная транзакция
|
||||
|
||||
TX_GROUP_ID_MISMATCH = не соответствие идентификатора группы c хэш транзации
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.qortal.test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -133,6 +137,129 @@ public class BlockTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLatestBlockCacheWithLatestBlock() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
|
||||
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
byte[] parentSignature = latestBlock.getSignature();
|
||||
|
||||
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
|
||||
|
||||
assertEquals(true, childBlocks.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLatestBlockCacheWithPenultimateBlock() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
|
||||
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
BlockData penultimateBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - 1);
|
||||
byte[] parentSignature = penultimateBlock.getSignature();
|
||||
|
||||
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
|
||||
|
||||
assertEquals(false, childBlocks.isEmpty());
|
||||
assertEquals(1, childBlocks.size());
|
||||
|
||||
BlockData expectedBlock = latestBlock;
|
||||
BlockData actualBlock = childBlocks.get(0);
|
||||
assertArrayEquals(expectedBlock.getSignature(), actualBlock.getSignature());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLatestBlockCacheWithMiddleBlock() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
|
||||
|
||||
int tipOffset = 5;
|
||||
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset);
|
||||
byte[] parentSignature = parentBlock.getSignature();
|
||||
|
||||
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
|
||||
|
||||
assertEquals(false, childBlocks.isEmpty());
|
||||
assertEquals(tipOffset, childBlocks.size());
|
||||
|
||||
BlockData expectedFirstBlock = repository.getBlockRepository().fromHeight(parentBlock.getHeight() + 1);
|
||||
BlockData actualFirstBlock = childBlocks.get(0);
|
||||
assertArrayEquals(expectedFirstBlock.getSignature(), actualFirstBlock.getSignature());
|
||||
|
||||
BlockData expectedLastBlock = latestBlock;
|
||||
BlockData actualLastBlock = childBlocks.get(childBlocks.size() - 1);
|
||||
assertArrayEquals(expectedLastBlock.getSignature(), actualLastBlock.getSignature());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLatestBlockCacheWithFirstBlock() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
|
||||
|
||||
int tipOffset = latestBlockCache.size();
|
||||
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset);
|
||||
byte[] parentSignature = parentBlock.getSignature();
|
||||
|
||||
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
|
||||
|
||||
assertEquals(false, childBlocks.isEmpty());
|
||||
assertEquals(tipOffset, childBlocks.size());
|
||||
|
||||
BlockData expectedFirstBlock = repository.getBlockRepository().fromHeight(parentBlock.getHeight() + 1);
|
||||
BlockData actualFirstBlock = childBlocks.get(0);
|
||||
assertArrayEquals(expectedFirstBlock.getSignature(), actualFirstBlock.getSignature());
|
||||
|
||||
BlockData expectedLastBlock = latestBlock;
|
||||
BlockData actualLastBlock = childBlocks.get(childBlocks.size() - 1);
|
||||
assertArrayEquals(expectedLastBlock.getSignature(), actualLastBlock.getSignature());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLatestBlockCacheWithNoncachedBlock() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
|
||||
|
||||
int tipOffset = latestBlockCache.size() + 1; // outside of cache
|
||||
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset);
|
||||
byte[] parentSignature = parentBlock.getSignature();
|
||||
|
||||
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
|
||||
|
||||
assertEquals(true, childBlocks.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
private Deque<BlockData> buildLatestBlockCache(Repository repository, int count) throws DataException {
|
||||
Deque<BlockData> latestBlockCache = new LinkedList<>();
|
||||
|
||||
// Mint some blocks
|
||||
for (int h = 0; h < count; ++h)
|
||||
latestBlockCache.addLast(BlockUtils.mintBlock(repository).getBlockData());
|
||||
|
||||
// Reduce cache down to latest 10 blocks
|
||||
while (latestBlockCache.size() > 10)
|
||||
latestBlockCache.removeFirst();
|
||||
|
||||
return latestBlockCache;
|
||||
}
|
||||
|
||||
private List<BlockData> findCachedChildBlocks(Deque<BlockData> latestBlockCache, byte[] parentSignature) {
|
||||
return latestBlockCache.stream()
|
||||
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommonBlockSearch() {
|
||||
// Given a list of block summaries, trim all trailing summaries after common block
|
||||
|
||||
@@ -103,8 +103,10 @@ public class ChainWeightTests extends Common {
|
||||
populateBlockSummariesMinterLevels(repository, shorterChain);
|
||||
populateBlockSummariesMinterLevels(repository, longerChain);
|
||||
|
||||
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain);
|
||||
BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain);
|
||||
final int mutualHeight = commonBlockHeight - 1 + Math.min(shorterChain.size(), longerChain.size());
|
||||
|
||||
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain, mutualHeight);
|
||||
BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain, mutualHeight);
|
||||
|
||||
assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight));
|
||||
}
|
||||
|
||||
@@ -15,12 +15,18 @@ import org.qortal.test.common.Common;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -127,6 +133,131 @@ public class RepositoryTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTrimDeadlock() {
|
||||
ExecutorService executor = Executors.newCachedThreadPool();
|
||||
CountDownLatch readyLatch = new CountDownLatch(1);
|
||||
CountDownLatch updateLatch = new CountDownLatch(1);
|
||||
CountDownLatch syncLatch = new CountDownLatch(1);
|
||||
|
||||
// Open connection 1
|
||||
try (final HSQLDBRepository repository1 = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
// Read AT states trim height
|
||||
int atTrimHeight = repository1.getATRepository().getAtTrimHeight();
|
||||
repository1.discardChanges();
|
||||
|
||||
// Open connection 2
|
||||
try (final HSQLDBRepository repository2 = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
// Read online signatures trim height
|
||||
int onlineSignaturesTrimHeight = repository2.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
|
||||
repository2.discardChanges();
|
||||
|
||||
Future<Boolean> f2 = executor.submit(() -> {
|
||||
Object trimHeightsLock = extractTrimHeightsLock(repository2);
|
||||
System.out.println(String.format("f2: repository2's trimHeightsLock object: %s", trimHeightsLock));
|
||||
|
||||
// Update online signatures trim height (implicit commit)
|
||||
synchronized (trimHeightsLock) {
|
||||
try {
|
||||
System.out.println("f2: updating online signatures trim height...");
|
||||
// simulate: repository2.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(onlineSignaturesTrimHeight);
|
||||
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
|
||||
PreparedStatement pstmt = repository2.prepareStatement(updateSql);
|
||||
pstmt.setInt(1, onlineSignaturesTrimHeight);
|
||||
pstmt.executeUpdate();
|
||||
// But no commit/saveChanges yet to force HSQLDB error
|
||||
|
||||
System.out.println("f2: readyLatch.countDown()");
|
||||
readyLatch.countDown();
|
||||
|
||||
// wait for other thread to be ready to hit sync block
|
||||
System.out.println("f2: waiting for f1 syncLatch...");
|
||||
syncLatch.await();
|
||||
|
||||
// hang on to trimHeightsLock to force other thread to wait (if code is correct), or to fail (if code is faulty)
|
||||
System.out.println("f2: updateLatch.await(<with timeout>)");
|
||||
if (!updateLatch.await(500L, TimeUnit.MILLISECONDS)) { // long enough for other thread to reach synchronized block
|
||||
// wait period expired suggesting no concurrent access, i.e. code is correct
|
||||
System.out.println("f2: updateLatch.await() timed out");
|
||||
|
||||
System.out.println("f2: saveChanges()");
|
||||
repository2.saveChanges();
|
||||
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
System.out.println("f2: saveChanges()");
|
||||
repository2.saveChanges();
|
||||
|
||||
// Early exit from wait period suggests concurrent access, i.e. code faulty
|
||||
return Boolean.FALSE;
|
||||
} catch (InterruptedException | SQLException e) {
|
||||
System.out.println("f2: exception: " + e.getMessage());
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
System.out.println("waiting for f2 readyLatch...");
|
||||
readyLatch.await();
|
||||
System.out.println("launching f1...");
|
||||
|
||||
Future<Boolean> f1 = executor.submit(() -> {
|
||||
Object trimHeightsLock = extractTrimHeightsLock(repository1);
|
||||
System.out.println(String.format("f1: repository1's trimHeightsLock object: %s", trimHeightsLock));
|
||||
|
||||
System.out.println("f1: syncLatch.countDown()");
|
||||
syncLatch.countDown();
|
||||
|
||||
// Update AT states trim height (implicit commit)
|
||||
synchronized (trimHeightsLock) {
|
||||
try {
|
||||
System.out.println("f1: updating AT trim height...");
|
||||
// simulate: repository1.getATRepository().setAtTrimHeight(atTrimHeight);
|
||||
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
|
||||
PreparedStatement pstmt = repository1.prepareStatement(updateSql);
|
||||
pstmt.setInt(1, atTrimHeight);
|
||||
pstmt.executeUpdate();
|
||||
System.out.println("f1: saveChanges()");
|
||||
repository1.saveChanges();
|
||||
|
||||
System.out.println("f1: updateLatch.countDown()");
|
||||
updateLatch.countDown();
|
||||
|
||||
return Boolean.TRUE;
|
||||
} catch (SQLException e) {
|
||||
System.out.println("f1: exception: " + e.getMessage());
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Boolean.TRUE != f1.get())
|
||||
fail("concurrency bug - simultaneous update of DatabaseInfo table");
|
||||
|
||||
if (Boolean.TRUE != f2.get())
|
||||
fail("concurrency bug - not synchronized on same object?");
|
||||
} catch (InterruptedException e) {
|
||||
fail("concurrency bug: " + e.getMessage());
|
||||
} catch (ExecutionException e) {
|
||||
fail("concurrency bug: " + e.getMessage());
|
||||
}
|
||||
} catch (DataException e) {
|
||||
fail("database bug");
|
||||
}
|
||||
}
|
||||
|
||||
private static Object extractTrimHeightsLock(HSQLDBRepository repository) {
|
||||
try {
|
||||
Field trimHeightsLockField = repository.getClass().getDeclaredField("trimHeightsLock");
|
||||
trimHeightsLockField.setAccessible(true);
|
||||
return trimHeightsLockField.get(repository);
|
||||
} catch (IllegalArgumentException | NoSuchFieldException | SecurityException | IllegalAccessException e) {
|
||||
fail();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Check that the <i>sub-query</i> used to fetch highest block height is optimized by HSQLDB. */
|
||||
@Test
|
||||
public void testBlockHeightSpeed() throws DataException, SQLException {
|
||||
|
||||
@@ -90,11 +90,11 @@ public class BlockApiTests extends ApiCommon {
|
||||
for (Integer endHeight : testValues)
|
||||
for (Integer count : testValues) {
|
||||
if (startHeight != null && endHeight != null && count != null) {
|
||||
assertApiError(ApiError.INVALID_CRITERIA, () -> this.blocksResource.getBlockRange(startHeight, endHeight, count));
|
||||
assertApiError(ApiError.INVALID_CRITERIA, () -> this.blocksResource.getBlockSummaries(startHeight, endHeight, count));
|
||||
continue;
|
||||
}
|
||||
|
||||
assertNotNull(this.blocksResource.getBlockRange(startHeight, endHeight, count));
|
||||
assertNotNull(this.blocksResource.getBlockSummaries(startHeight, endHeight, count));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
426
src/test/java/org/qortal/test/at/AtRepositoryTests.java
Normal file
426
src/test/java/org/qortal/test/at/AtRepositoryTests.java
Normal file
@@ -0,0 +1,426 @@
|
||||
package org.qortal.test.at;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
|
||||
public class AtRepositoryTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetATStateAtHeightWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
Integer testHeight = 8;
|
||||
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetATStateAtHeightWithoutData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
int maxHeight = 8;
|
||||
Integer testHeight = maxHeight - 2;
|
||||
|
||||
// Trim AT state data
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
|
||||
|
||||
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLatestATStateWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
Integer testHeight = blockchainHeight;
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLatestATStatePostTrimming() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
int maxHeight = blockchainHeight + 100; // more than latest block height
|
||||
Integer testHeight = blockchainHeight;
|
||||
|
||||
// Trim AT state data
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
|
||||
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
// We should always have the latest AT state data available
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMatchingFinalATStatesWithoutDataValue() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
Integer testHeight = blockchainHeight;
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
|
||||
byte[] codeHash = atData.getCodeHash();
|
||||
Boolean isFinished = Boolean.FALSE;
|
||||
Integer dataByteOffset = null;
|
||||
Long expectedValue = null;
|
||||
Integer minimumFinalHeight = null;
|
||||
Integer limit = null;
|
||||
Integer offset = null;
|
||||
Boolean reverse = null;
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
|
||||
codeHash,
|
||||
isFinished,
|
||||
dataByteOffset,
|
||||
expectedValue,
|
||||
minimumFinalHeight,
|
||||
limit, offset, reverse);
|
||||
|
||||
assertEquals(false, atStates.isEmpty());
|
||||
assertEquals(1, atStates.size());
|
||||
|
||||
ATStateData atStateData = atStates.get(0);
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMatchingFinalATStatesWithDataValue() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
Integer testHeight = blockchainHeight;
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
|
||||
byte[] codeHash = atData.getCodeHash();
|
||||
Boolean isFinished = Boolean.FALSE;
|
||||
Integer dataByteOffset = MachineState.HEADER_LENGTH + 0;
|
||||
Long expectedValue = 0L;
|
||||
Integer minimumFinalHeight = null;
|
||||
Integer limit = null;
|
||||
Integer offset = null;
|
||||
Boolean reverse = null;
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
|
||||
codeHash,
|
||||
isFinished,
|
||||
dataByteOffset,
|
||||
expectedValue,
|
||||
minimumFinalHeight,
|
||||
limit, offset, reverse);
|
||||
|
||||
assertEquals(false, atStates.isEmpty());
|
||||
assertEquals(1, atStates.size());
|
||||
|
||||
ATStateData atStateData = atStates.get(0);
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBlockATStatesAtHeightWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
Integer testHeight = 8;
|
||||
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
|
||||
|
||||
assertEquals(false, atStates.isEmpty());
|
||||
assertEquals(1, atStates.size());
|
||||
|
||||
ATStateData atStateData = atStates.get(0);
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
// getBlockATStatesAtHeight never returns actual AT state data anyway
|
||||
assertNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBlockATStatesAtHeightWithoutData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
int maxHeight = 8;
|
||||
Integer testHeight = maxHeight - 2;
|
||||
|
||||
// Trim AT state data
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
|
||||
|
||||
assertEquals(false, atStates.isEmpty());
|
||||
assertEquals(1, atStates.size());
|
||||
|
||||
ATStateData atStateData = atStates.get(0);
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
// getBlockATStatesAtHeight never returns actual AT state data anyway
|
||||
assertNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveATStateWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
Integer testHeight = blockchainHeight - 2;
|
||||
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
|
||||
repository.getATRepository().save(atStateData);
|
||||
|
||||
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveATStateWithoutData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
Integer testHeight = blockchainHeight - 2;
|
||||
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNotNull(atStateData.getStateData());
|
||||
|
||||
// Clear data
|
||||
ATStateData newAtStateData = new ATStateData(atStateData.getATAddress(),
|
||||
atStateData.getHeight(),
|
||||
/*StateData*/ null,
|
||||
atStateData.getStateHash(),
|
||||
atStateData.getFees(),
|
||||
atStateData.isInitial());
|
||||
repository.getATRepository().save(newAtStateData);
|
||||
|
||||
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
assertNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] buildSimpleAT() {
|
||||
// Pretend we use 4 values in data segment
|
||||
int addrCounter = 4;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
final short ciyamAtVersion = 2;
|
||||
final short numCallStackPages = 0;
|
||||
final short numUserStackPages = 0;
|
||||
final long minActivationAmount = 0L;
|
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||
|
||||
return deployAtTransaction;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@ fi
|
||||
cd ${git_dir}
|
||||
|
||||
# Check we are in 'master' branch
|
||||
branch_name=$( git symbolic-ref -q HEAD )
|
||||
branch_name=$( git symbolic-ref -q HEAD || echo )
|
||||
branch_name=${branch_name##refs/heads/}
|
||||
echo "Current git branch: ${branch_name}"
|
||||
if [ "${branch_name}" != "master" ]; then
|
||||
@@ -78,5 +78,7 @@ 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}
|
||||
|
||||
branch_name=${branch_name-master}
|
||||
|
||||
echo "Changing back to '${branch_name}' branch"
|
||||
git checkout --force ${branch_name}
|
||||
|
||||
Reference in New Issue
Block a user