Compare commits

...

39 Commits

Author SHA1 Message Date
CalDescent
d22e97ffc8 Fixed build issues due to merge. 2023-02-10 18:13:42 +00:00
CalDescent
597fbce9b0 Added chatdb and started separating chat messages from transactions. Work in progress. 2023-02-10 17:58:31 +00:00
CalDescent
830bae3dc1 Merge branch 'at-states-fix'
# Conflicts:
#	src/main/java/org/qortal/controller/repository/AtStatesPruner.java
#	src/main/java/org/qortal/controller/repository/AtStatesTrimmer.java
2023-02-10 17:45:22 +00:00
CalDescent
ec09312cc5 Updated AdvancedInstaller project for 3.8.5 2023-02-10 17:42:12 +00:00
CalDescent
e7a3e511bd Bump version to 3.8.5 2023-02-08 19:37:01 +00:00
CalDescent
6fca30ce75 Added GET /admin/summary/alltime endpoint, to view a summary of chain activity since genesis. 2023-02-07 19:56:54 +00:00
CalDescent
e903e59f7f Merge pull request #107 from QuickMythril/unused-address
Add unused foreign address to API calls
2023-02-06 18:43:22 +00:00
CalDescent
bef170df7e Updated PirateChain lightwallet servers. 2023-02-06 18:42:37 +00:00
QuickMythril
386bfa4e20 Merge pull request #108 from AlphaX-Projects/master
Add electrum servers and fix java reflect error
2023-02-05 07:38:30 -05:00
AlphaX-Projects
6f867031e2 Add electrum servers and fix java reflect error 2023-02-05 12:53:49 +01:00
QuickMythril
8f589391a6 Updated depreciated actions
Node.js 12 actions are deprecated. Please update the following actions to use Node.js 16: actions/checkout@v2, actions/cache@v2, actions/setup-java@v2. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/
2023-02-04 21:57:31 -05:00
QuickMythril
30c9f63cb1 Add unused foreign address to API calls
POST ​/crosschain​/{COIN}/unusedaddress
2023-02-04 21:03:55 -05:00
QuickMythril
952b21d9bd Merge pull request #105 from QuickMythril/update-electrumx
Updated ElectrumX servers
2023-02-04 18:40:39 -05:00
QuickMythril
1f410a503e Updated ElectrumX servers 2023-02-04 18:30:31 -05:00
CalDescent
f5e30eeaf5 Merge pull request #104 from QuickMythril/foreign-height
Add foreign chain height to API calls
2023-02-01 20:28:55 +00:00
QuickMythril
21f5d9a3d0 Add foreign chain height to API calls
GET ​/crosschain​/{COIN}/height
2023-01-31 17:23:25 -05:00
CalDescent
ab34fae810 Merge pull request #90 from QuickMythril/german
Updated German translations
2023-01-28 20:22:11 +00:00
CalDescent
42f2d015b7 Merge branch 'master' into german 2023-01-28 20:22:02 +00:00
CalDescent
2181ece28d Merge pull request #89 from lexandr0s/patch-2
Update ApiError_ru.properties
2023-01-28 20:21:13 +00:00
CalDescent
03a5d0e5f9 Merge pull request #88 from lexandr0s/patch-1
Update SysTray_ru.properties
2023-01-28 20:21:00 +00:00
CalDescent
352f094272 Merge pull request #99 from Nuc1eoN/polish-translation
Add polish translation
2023-01-28 20:20:35 +00:00
CalDescent
c5c826453b Removed unnecessary join when finding MESSAGE transactions, which caused secret to be unavailable when querying pruned blocks. 2023-01-28 15:41:48 +00:00
CalDescent
e86b9b1caf Added additional Litecoin ElectrumX server. 2023-01-28 15:34:30 +00:00
CalDescent
7fc170575c Merge branch 'cancel-sell-name-fixes' 2023-01-28 12:11:42 +00:00
CalDescent
876658256f Prevent a P2SH address being funded for a trade if there is an unconfirmed buy or cancel request in progress for it already.
This prevents foreign coins from leaving the local wallet when there is a high probability that the trade will fail, and therefore should reduce the chances of losing transaction fees due to refunds.

Whenever this occurs, the UI will show "Trade has an existing buy request or is pending cancellation." after clicking Buy.
2023-01-28 11:57:15 +00:00
CalDescent
a24ba40d5c Added additional Dogecoin ElectrumX server. 2023-01-28 09:54:15 +00:00
CalDescent
06d8a21714 Added CANCEL_SELL_NAME equivalents to NamesDatabaseIntegrityCheck.java 2023-01-27 19:38:26 +00:00
CalDescent
ae44065d7e Fixed issue with CancelSellName transactions. 2023-01-27 19:34:23 +00:00
CalDescent
6ad0989ea2 Reduce log spam 2023-01-27 18:35:44 +00:00
CalDescent
5962ebd08a More logging improvements in ArbitraryDataReader.decrypt() 2023-01-27 16:56:53 +00:00
CalDescent
bf06d47842 Create an ArbitraryDataResource object when building. Eventually this could be passed in to the reader instead of the individual components (service, name, identifier, etc)
This is now used to improve logging when extracting.
2023-01-27 16:55:43 +00:00
CalDescent
8c708558cb Implemented ElectrumX version negotiation. Fixes issues with DOGE wallet. 2023-01-27 14:33:34 +00:00
CalDescent
6b36d94c6f Removed searchResultsTransactions cache, to simplify code. The hostedTransactions cache is still in place, which limits disk reads when searching, so this additional cache isn't really needed. 2023-01-27 12:48:42 +00:00
CalDescent
de47a94677 Fixed bug causing initial latestATStates data to be discarded. 2023-01-15 15:51:10 +00:00
CalDescent
bd4c47dba6 Rework of AT state trimming and pruning, in order to more reliably track the "latest" AT states.
This should fix an edge case where AT states data was pruned/trimmed but it was then later required in consensus. The older state was deleted because it was replaced by a new "latest" state in a brand new block. But once the new "latest" state was orphaned from the block, the old "latest" state was then required again.

This works around the problem by excluding very recent blocks in the latest AT states data, so that it is unaffected by real-time sync activity.

The trade off is that we could end up retaining more AT states than needed, so a secondary cleanup process may need to run at some time in the future to remove these. But it should only be a minimal amount of data, and can be cleaned up with a single query. This would have been happening to a certain degree already.
2023-01-15 14:32:33 +00:00
Nuc1eoN
8ddf4c9f9f Add polish translation 2022-10-09 15:35:19 +02:00
QuickMythril
ff40b8f8ab Updated German translations 2022-06-23 01:43:33 -04:00
lexandr0s
c03344caae Update ApiError_ru.properties 2022-06-04 23:57:25 +04:00
lexandr0s
237b39a524 Update SysTray_ru.properties 2022-06-04 23:50:03 +04:00
70 changed files with 2737 additions and 269 deletions

View File

@@ -8,16 +8,16 @@ jobs:
mavenTesting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Cache local Maven repository
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Set up the Java JDK
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'adopt'

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/db*
/chatdb*
/lists/
/bin/
/target/

View File

@@ -17,10 +17,10 @@
<ROW Property="Manufacturer" Value="Qortal"/>
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
<ROW Property="NTP_GOOD" Value="false"/>
<ROW Property="ProductCode" Value="1033:{6C93A96C-E3AF-42FD-BE11-7EC3734905C6} 1049:{754F5347-82E5-4251-AED0-F4141CDD11F5} 2052:{413BD7B3-A3F8-47D0-BCA4-5C7694A40936} 2057:{71450AC8-1E6F-4469-852D-0591FA693680} " Type="16"/>
<ROW Property="ProductCode" Value="1033:{CB85115E-ECCE-4B3D-BB7F-6251A2764922} 1049:{09AC1C62-4E33-4312-826A-38F597ED1B17} 2052:{3CF701B3-E118-4A31-A4B7-156CEA19FBCC} 2057:{468F337D-0EF8-41D1-B5DE-4EEE66BA2AF6} " Type="16"/>
<ROW Property="ProductLanguage" Value="2057"/>
<ROW Property="ProductName" Value="Qortal"/>
<ROW Property="ProductVersion" Value="3.8.3" Type="32"/>
<ROW Property="ProductVersion" Value="3.8.5" Type="32"/>
<ROW Property="RECONFIG_NTP" Value="true"/>
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
@@ -212,7 +212,7 @@
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
<ROW Component="AI_CustomARPName" ComponentId="{EC7B4AD9-F2D9-48C4-A586-C4697D9C380C}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
<ROW Component="AI_CustomARPName" ComponentId="{094B5D07-2258-4A39-9917-2E2F7F6E210B}" 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="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>3.8.4</version>
<version>3.8.5</version>
<packaging>jar</packaging>
<properties>
<skipTests>true</skipTests>
@@ -304,6 +304,7 @@
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.qortal.controller.Controller</mainClass>
<manifestEntries>
<Multi-Release>true</Multi-Release>
<Class-Path>. ..</Class-Path>
</manifestEntries>
</transformer>

View File

@@ -222,6 +222,42 @@ public class AdminResource {
}
}
@GET
@Path("/summary/alltime")
@Operation(
summary = "Summary of activity since genesis",
responses = {
@ApiResponse(
content = @Content(schema = @Schema(implementation = ActivitySummary.class))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public ActivitySummary allTimeSummary(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
Security.checkApiCallAllowed(request);
ActivitySummary summary = new ActivitySummary();
try (final Repository repository = RepositoryManager.getRepository()) {
int startHeight = 1;
long start = repository.getBlockRepository().fromHeight(startHeight).getTimestamp();
int endHeight = repository.getBlockRepository().getBlockchainHeight();
summary.setBlockCount(endHeight - startHeight);
summary.setTransactionCountByType(repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight));
summary.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(start).size());
summary.setNamesRegistered (repository.getNameRepository().getRecentNames(start).size());
return summary;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/enginestats")
@Operation(

View File

@@ -134,12 +134,7 @@ public class ChatResource {
try (final Repository repository = RepositoryManager.getRepository()) {
ChatTransactionData chatTransactionData = (ChatTransactionData) repository.getTransactionRepository().fromSignature(signature);
if (chatTransactionData == null) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Message not found");
}
return repository.getChatRepository().toChatMessage(chatTransactionData);
return repository.getChatRepository().getChatMessageBySignature(signature);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}

View File

@@ -14,6 +14,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -35,6 +36,37 @@ public class CrossChainBitcoinResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current Bitcoin block height",
description = "Returns the height of the most recent block in the Bitcoin chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getBitcoinHeight() {
Bitcoin bitcoin = Bitcoin.getInstance();
try {
Integer height = bitcoin.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(
@@ -118,6 +150,45 @@ public class CrossChainBitcoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedBitcoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Bitcoin bitcoin = Bitcoin.getInstance();
if (!bitcoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return bitcoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(

View File

@@ -14,6 +14,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -35,6 +36,37 @@ public class CrossChainDigibyteResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current Digibyte block height",
description = "Returns the height of the most recent block in the Digibyte chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getDigibyteHeight() {
Digibyte digibyte = Digibyte.getInstance();
try {
Integer height = digibyte.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(
@@ -118,6 +150,45 @@ public class CrossChainDigibyteResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedDigibyteReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Digibyte digibyte = Digibyte.getInstance();
if (!digibyte.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return digibyte.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(

View File

@@ -21,6 +21,7 @@ import org.qortal.crosschain.SimpleTransaction;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -33,6 +34,37 @@ public class CrossChainDogecoinResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current Dogecoin block height",
description = "Returns the height of the most recent block in the Dogecoin chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getDogecoinHeight() {
Dogecoin dogecoin = Dogecoin.getInstance();
try {
Integer height = dogecoin.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(
@@ -116,6 +148,45 @@ public class CrossChainDogecoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedDogecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Dogecoin dogecoin = Dogecoin.getInstance();
if (!dogecoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return dogecoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(

View File

@@ -14,6 +14,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -35,6 +36,37 @@ public class CrossChainLitecoinResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current Litecoin block height",
description = "Returns the height of the most recent block in the Litecoin chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getLitecoinHeight() {
Litecoin litecoin = Litecoin.getInstance();
try {
Integer height = litecoin.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(
@@ -118,6 +150,45 @@ public class CrossChainLitecoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedLitecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Litecoin litecoin = Litecoin.getInstance();
if (!litecoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return litecoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(

View File

@@ -20,6 +20,7 @@ import org.qortal.crosschain.SimpleTransaction;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -32,6 +33,37 @@ public class CrossChainPirateChainResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current PirateChain block height",
description = "Returns the height of the most recent block in the PirateChain chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getPirateChainHeight() {
PirateChain pirateChain = PirateChain.getInstance();
try {
Integer height = pirateChain.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(

View File

@@ -14,6 +14,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -35,6 +36,37 @@ public class CrossChainRavencoinResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current Ravencoin block height",
description = "Returns the height of the most recent block in the Ravencoin chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getRavencoinHeight() {
Ravencoin ravencoin = Ravencoin.getInstance();
try {
Integer height = ravencoin.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(
@@ -118,6 +150,45 @@ public class CrossChainRavencoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedRavencoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Ravencoin ravencoin = Ravencoin.getInstance();
if (!ravencoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return ravencoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(

View File

@@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@@ -38,9 +39,12 @@ import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.Transaction;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
@@ -223,6 +227,17 @@ public class CrossChainTradeBotResource {
if (crossChainTradeData.mode != AcctMode.OFFERING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Check if there is a buy or a cancel request in progress for this trade
List<Transaction.TransactionType> txTypes = List.of(Transaction.TransactionType.MESSAGE);
List<TransactionData> unconfirmed = repository.getTransactionRepository().getUnconfirmedTransactions(txTypes, null, 0, 0, false);
for (TransactionData transactionData : unconfirmed) {
MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData;
if (Objects.equals(messageTransactionData.getRecipient(), atAddress)) {
// There is a pending request for this trade, so block this buy attempt to reduce the risk of refunds
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Trade has an existing buy request or is pending cancellation.");
}
}
AcctTradeBot.ResponseResult result = TradeBot.getInstance().startResponse(repository, atData, acct, crossChainTradeData,
tradeBotRespondRequest.foreignKey, tradeBotRespondRequest.receivingAddress);

View File

@@ -16,6 +16,7 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.ChatNotifier;
import org.qortal.crypto.Crypto;
import org.qortal.data.chat.ActiveChats;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -43,7 +44,7 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
AtomicReference<String> previousOutput = new AtomicReference<>(null);
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, address, previousOutput);
ChatNotifier.Listener listener = chatMessage -> onNotify(session, chatMessage, address, previousOutput);
ChatNotifier.getInstance().register(session, listener);
this.onNotify(session, null, address, previousOutput);
@@ -65,12 +66,12 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
/* ignored */
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, String ourAddress, AtomicReference<String> previousOutput) {
private void onNotify(Session session, ChatMessage chatMessage, String ourAddress, AtomicReference<String> previousOutput) {
// If CHAT has a recipient (i.e. direct message, not group-based) and we're neither sender nor recipient, then it's of no interest
if (chatTransactionData != null) {
String recipient = chatTransactionData.getRecipient();
if (chatMessage != null) {
String recipient = chatMessage.getRecipient();
if (recipient != null && (!recipient.equals(ourAddress) && !chatTransactionData.getSender().equals(ourAddress)))
if (recipient != null && (!recipient.equals(ourAddress) && !chatMessage.getSender().equals(ourAddress)))
return;
}

View File

@@ -17,7 +17,6 @@ import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.ChatNotifier;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -58,7 +57,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
return;
}
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, txGroupId);
ChatNotifier.Listener listener = chatMessage -> onNotify(session, chatMessage, txGroupId);
ChatNotifier.getInstance().register(session, listener);
return;
@@ -108,33 +107,33 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
/* ignored */
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, int txGroupId) {
if (chatTransactionData == null)
private void onNotify(Session session, ChatMessage chatMessage, int txGroupId) {
if (chatMessage == null)
// There has been a group-membership change, but we're not interested
return;
// We only want group-based messages with our txGroupId
if (chatTransactionData.getRecipient() != null || chatTransactionData.getTxGroupId() != txGroupId)
if (chatMessage.getRecipient() != null || chatMessage.getTxGroupId() != txGroupId)
return;
sendChat(session, chatTransactionData);
sendChat(session, chatMessage);
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, List<String> involvingAddresses) {
if (chatTransactionData == null)
private void onNotify(Session session, ChatMessage chatMessage, List<String> involvingAddresses) {
if (chatMessage == null)
return;
// We only want direct/non-group messages where sender/recipient match our addresses
String recipient = chatTransactionData.getRecipient();
String recipient = chatMessage.getRecipient();
if (recipient == null)
return;
List<String> transactionAddresses = Arrays.asList(recipient, chatTransactionData.getSender());
List<String> transactionAddresses = Arrays.asList(recipient, chatMessage.getSender());
if (!transactionAddresses.containsAll(involvingAddresses))
return;
sendChat(session, chatTransactionData);
sendChat(session, chatMessage);
}
private void sendMessages(Session session, List<ChatMessage> chatMessages) {
@@ -149,16 +148,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
}
}
private void sendChat(Session session, ChatTransactionData chatTransactionData) {
// Convert ChatTransactionData to ChatMessage
ChatMessage chatMessage;
try (final Repository repository = RepositoryManager.getRepository()) {
chatMessage = repository.getChatRepository().toChatMessage(chatTransactionData);
} catch (DataException e) {
// No output this time?
return;
}
private void sendChat(Session session, ChatMessage chatMessage) {
sendMessages(session, Collections.singletonList(chatMessage));
}

View File

@@ -60,6 +60,9 @@ public class ArbitraryDataReader {
private int layerCount;
private byte[] latestSignature;
// The resource being read
ArbitraryDataResource arbitraryDataResource = null;
public ArbitraryDataReader(String resourceId, ResourceIdType resourceIdType, Service service, String identifier) {
// Ensure names are always lowercase
if (resourceIdType == ResourceIdType.NAME) {
@@ -116,6 +119,11 @@ public class ArbitraryDataReader {
return new ArbitraryDataBuildQueueItem(this.resourceId, this.resourceIdType, this.service, this.identifier);
}
private ArbitraryDataResource createArbitraryDataResource() {
return new ArbitraryDataResource(this.resourceId, this.resourceIdType, this.service, this.identifier);
}
/**
* loadAsynchronously
*
@@ -163,6 +171,8 @@ public class ArbitraryDataReader {
return;
}
this.arbitraryDataResource = this.createArbitraryDataResource();
this.preExecute();
this.deleteExistingFiles();
this.fetch();
@@ -436,7 +446,7 @@ public class ArbitraryDataReader {
byte[] secret = this.secret58 != null ? Base58.decode(this.secret58) : null;
if (secret != null && secret.length == Transformer.AES256_LENGTH) {
try {
LOGGER.info("Decrypting using algorithm {}...", algorithm);
LOGGER.debug("Decrypting {} using algorithm {}...", this.arbitraryDataResource, algorithm);
Path unencryptedPath = Paths.get(this.workingPath.toString(), "zipped.zip");
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, "AES");
AES.decryptFile(algorithm, aesKey, this.filePath.toString(), unencryptedPath.toString());
@@ -447,7 +457,7 @@ public class ArbitraryDataReader {
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException
| BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) {
LOGGER.info(String.format("Exception when decrypting using algorithm %s", algorithm), e);
LOGGER.info(String.format("Exception when decrypting %s using algorithm %s", this.arbitraryDataResource, algorithm), e);
throw new DataException(String.format("Unable to decrypt file at path %s using algorithm %s: %s", this.filePath, algorithm, e.getMessage()));
}
} else {

View File

@@ -0,0 +1,42 @@
package org.qortal.controller;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class ChatManager extends Thread {
private static final Logger LOGGER = LogManager.getLogger(ChatManager.class);
private static ChatManager instance;
private volatile boolean isStopping = false;
public ChatManager() {
}
public static synchronized ChatManager getInstance() {
if (instance == null) {
instance = new ChatManager();
}
return instance;
}
public void run() {
try {
while (!Controller.isStopping()) {
Thread.sleep(100L);
}
} catch (InterruptedException e) {
// Fall through to exit thread
}
}
public void shutdown() {
isStopping = true;
this.interrupt();
}
}

View File

@@ -6,6 +6,7 @@ import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.transaction.ChatTransactionData;
public class ChatNotifier {
@@ -14,7 +15,7 @@ public class ChatNotifier {
@FunctionalInterface
public interface Listener {
void notify(ChatTransactionData chatTransactionData);
void notify(ChatMessage chatMessage);
}
private Map<Session, Listener> listenersBySession = new HashMap<>();
@@ -41,9 +42,9 @@ public class ChatNotifier {
}
}
public void onNewChatTransaction(ChatTransactionData chatTransactionData) {
public void onNewChatMessage(ChatMessage chatMessage) {
for (Listener listener : getAllListeners())
listener.notify(chatTransactionData);
listener.notify(chatMessage);
}
public void onGroupMembershipChange() {

View File

@@ -45,9 +45,9 @@ import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.naming.NameData;
import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
@@ -57,14 +57,17 @@ import org.qortal.gui.SysTray;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.message.*;
import org.qortal.network.message.GetChatMessagesMessage.Direction;
import org.qortal.repository.*;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transform.TransformationException;
import org.qortal.utils.*;
import static org.qortal.network.Network.MAX_CHAT_MESSAGES_PER_REPLY;
import static org.qortal.repository.hsqldb.HSQLDBRepositoryFactory.HSQLDBRepositoryType.*;
public class Controller extends Thread {
static {
@@ -82,6 +85,7 @@ public class Controller extends Thread {
private static final int MAX_BLOCKCHAIN_TIP_AGE = 5; // blocks
private static final Object shutdownLock = new Object();
private static final String repositoryUrlTemplate = "jdbc:hsqldb:file:%s" + File.separator + "blockchain;create=true;hsqldb.full_log_replay=true";
private static final String chatRepositoryUrlTemplate = "jdbc:hsqldb:file:%s" + File.separator + "chat;create=true;hsqldb.full_log_replay=true";
private static final long NTP_PRE_SYNC_CHECK_PERIOD = 5 * 1000L; // ms
private static final long NTP_POST_SYNC_CHECK_PERIOD = 5 * 60 * 1000L; // ms
private static final long DELETE_EXPIRED_INTERVAL = 5 * 60 * 1000L; // ms
@@ -230,6 +234,31 @@ public class Controller extends Thread {
}
public GetNameMessageStats getNameMessageStats = new GetNameMessageStats();
public static class GetChatMessagesStats {
public AtomicLong requests = new AtomicLong();
public GetChatMessagesStats() {
}
}
public GetChatMessagesStats getChatMessagesStats = new GetChatMessagesStats();
public static class GetChatMessageStats {
public AtomicLong requests = new AtomicLong();
public AtomicLong unknownMessages = new AtomicLong();
public GetChatMessageStats() {
}
}
public GetChatMessageStats getChatMessageStats = new GetChatMessageStats();
public static class GetRecentChatMessagesStats {
public AtomicLong requests = new AtomicLong();
public GetRecentChatMessagesStats() {
}
}
public GetRecentChatMessagesStats getRecentChatMessagesStats = new GetRecentChatMessagesStats();
public AtomicLong latestBlocksCacheRefills = new AtomicLong();
public StatsSnapshot() {
@@ -294,6 +323,10 @@ public class Controller extends Thread {
return String.format(repositoryUrlTemplate, Settings.getInstance().getRepositoryPath());
}
public static String getChatRepositoryUrl() {
return String.format(chatRepositoryUrlTemplate, Settings.getInstance().getChatRepositoryPath());
}
public long getBuildTimestamp() {
return this.buildTimestamp;
}
@@ -397,7 +430,7 @@ public class Controller extends Thread {
LOGGER.info("Starting repository");
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(getRepositoryUrl());
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(getRepositoryUrl(), MAIN);
RepositoryManager.setRepositoryFactory(repositoryFactory);
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
@@ -418,6 +451,24 @@ public class Controller extends Thread {
return; // Not System.exit() so that GUI can display error
}
LOGGER.info("Starting chat repository");
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(getChatRepositoryUrl(), CHAT);
ChatRepositoryManager.setRepositoryFactory(repositoryFactory);
ChatRepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
} catch (DataException e) {
// If exception has no cause then repository is in use by some other process.
if (e.getCause() == null) {
LOGGER.info("Chat repository in use by another process?");
Gui.getInstance().fatalError("Chat repository issue", "Chat repository in use by another process?");
} else {
LOGGER.error("Unable to start chat repository", e);
Gui.getInstance().fatalError("Chat repository issue", e);
}
return; // Not System.exit() so that GUI can display error
}
// If we have a non-lite node, we need to perform some startup actions
if (!Settings.getInstance().isLite()) {
@@ -606,6 +657,7 @@ public class Controller extends Thread {
repositoryCheckpointTimestamp = now + repositoryCheckpointInterval;
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
ChatRepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
}
// Give repository a chance to backup (if enabled)
@@ -991,6 +1043,13 @@ public class Controller extends Thread {
LOGGER.error("Error occurred while shutting down repository", e);
}
try {
LOGGER.info("Shutting down chat repository");
ChatRepositoryManager.closeRepositoryFactory();
} catch (DataException e) {
LOGGER.error("Error occurred while shutting down chat repository", e);
}
// Release the lock if we acquired it
if (blockchainLock.isHeldByCurrentThread()) {
blockchainLock.unlock();
@@ -1041,10 +1100,15 @@ public class Controller extends Thread {
// Send our current height
network.broadcastOurChain();
// Request unconfirmed transaction signatures, but only if we're up-to-date.
// Request unconfirmed transaction signatures and chat messages, but only if we're up-to-date.
// If we're NOT up-to-date then priority is synchronizing first
if (isUpToDate())
if (isUpToDate()) {
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
// Build the message only once, as it requires heavy db calls
Message chatMessageSignaturesMessage = network.buildChatMessageSignaturesMessage();
network.broadcast(peer -> peer.getPeersVersion() >= ChatMessageSignaturesMessage.MIN_PEER_VERSION ? chatMessageSignaturesMessage : null);
}
}
public void onMintingPossibleChange(boolean isMintingPossible) {
@@ -1201,12 +1265,31 @@ public class Controller extends Thread {
// Notify listeners
EventBus.INSTANCE.notify(new NewTransactionEvent(transactionData));
// If this is a CHAT transaction, there may be extra listeners to notify
if (transactionData.getType() == TransactionType.CHAT)
ChatNotifier.getInstance().onNewChatTransaction((ChatTransactionData) transactionData);
// // If this is a CHAT transaction, there may be extra listeners to notify
// if (transactionData.getType() == TransactionType.CHAT)
// ChatNotifier.getInstance().onNewChatTransaction((ChatTransactionData) transactionData);
// TODO: bridge CHAT messages to new db
});
}
// TODO: call this when sending new messages (as well as calling save())
public void onNewChatMessage(ChatMessage chatMessage) {
onNewChatMessages(Arrays.asList(chatMessage));
}
public void onNewChatMessages(List<ChatMessage> chatMessages) {
List<byte[]> signatures = chatMessages.stream().map(ChatMessage::getSignature).collect(Collectors.toList());
// Notify all peers
Message newChatMessageSignatureMessage = new ChatMessageSignaturesMessage(signatures);
Network.getInstance().broadcast(broadcastPeer -> newChatMessageSignatureMessage);
// Notify listeners
for (ChatMessage chatMessage : chatMessages) {
ChatNotifier.getInstance().onNewChatMessage(chatMessage);
}
}
public void onPeerHandshakeCompleted(Peer peer) {
// Only send if outbound
if (peer.isOutbound()) {
@@ -1337,6 +1420,26 @@ public class Controller extends Thread {
onNetworkGetNameMessage(peer, message);
break;
case CHAT_MESSAGE_SIGNATURES:
onNetworkChatMessageSignaturesMessage(peer, message);
break;
case CHAT_MESSAGES:
onNetworkChatMessagesMessage(peer, message);
break;
case GET_CHAT_MESSAGES:
onNetworkGetChatMessagesMessage(peer, message);
break;
case GET_CHAT_MESSAGE:
onNetworkGetChatMessageMessage(peer, message);
break;
case GET_RECENT_CHAT_MESSAGES:
onNetworkGetRecentChatMessagesMessage(peer, message);
break;
default:
LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer));
break;
@@ -1837,6 +1940,160 @@ public class Controller extends Thread {
}
}
private void onNetworkGetChatMessagesMessage(Peer peer, Message message) {
GetChatMessagesMessage getChatMessagesMessage = (GetChatMessagesMessage) message;
final long timestamp = getChatMessagesMessage.getTimestamp();
final Direction direction = getChatMessagesMessage.getDirection();
this.stats.getChatMessagesStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
int numberRequested = Math.min(MAX_CHAT_MESSAGES_PER_REPLY, getChatMessagesMessage.getNumberRequested());
Long before = (direction == Direction.BACKWARDS ? timestamp : null);
Long after = (direction == Direction.FORWARDS ? timestamp : null);
boolean reverse = (direction == Direction.BACKWARDS);
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
before, after, null, null, null, null, null, numberRequested, 0, reverse);
Message chatMessagesMessage = new ChatMessagesMessage(chatMessages);
chatMessagesMessage.setId(message.getId());
if (!peer.sendMessage(chatMessagesMessage))
peer.disconnect("failed to send chat messages");
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending chat messages to peer %s", peer), e);
}
}
private void onNetworkGetChatMessageMessage(Peer peer, Message message) {
GetChatMessageMessage getChatMessageMessage = (GetChatMessageMessage) message;
byte[] signature = getChatMessageMessage.getSignature();
this.stats.getChatMessageStats.requests.incrementAndGet();
try (final Repository repository = ChatRepositoryManager.getRepository()) {
ChatMessage chatMessage = repository.getChatRepository().getChatMessageBySignature(signature);
if (chatMessage == null) {
// We don't have this message
this.stats.getChatMessageStats.unknownMessages.getAndIncrement();
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'message unknown' response to peer %s for GET_CHAT_MESSAGE request for unknown signature %s", peer, Base58.encode(signature)));
// We'll send empty block summaries message as it's very short
Message nameUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
nameUnknownMessage.setId(message.getId());
if (!peer.sendMessage(nameUnknownMessage))
peer.disconnect("failed to send message-unknown response");
return;
}
Message chatMessagesMessage = new ChatMessagesMessage(Arrays.asList(chatMessage));
chatMessagesMessage.setId(message.getId());
if (!peer.sendMessage(chatMessagesMessage))
peer.disconnect("failed to send chat messages");
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending chat message to peer %s", peer), e);
}
}
private void onNetworkGetRecentChatMessagesMessage(Peer peer, Message message) {
GetRecentChatMessagesMessage getRecentChatMessagesMessage = (GetRecentChatMessagesMessage) message;
final List<ByteArray> signatures = getRecentChatMessagesMessage.getSignatures();
this.stats.getRecentChatMessagesStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
List<ChatMessage> chatMessages = new ArrayList<>();
// Don't request further back than 24 hours
long after = NTP.getTime() - (24 * 60 * 60 * 1000L);
// Get all recent messages from repository
List<ChatMessage> ourChatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
null, after, null, null, null, null, null, null, 0, true);
for (ChatMessage chatMessage : ourChatMessages) {
// Skip if the sender already has this one
if (signatures.contains(chatMessage.getSignature())) {
continue;
}
chatMessages.add(chatMessage);
if (chatMessages.size() >= MAX_CHAT_MESSAGES_PER_REPLY) {
// Don't send any more
break;
}
}
Message chatMessagesMessage = new ChatMessagesMessage(chatMessages);
chatMessagesMessage.setId(message.getId());
if (!peer.sendMessage(chatMessagesMessage))
peer.disconnect("failed to send chat messages");
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending recent chat messages to peer %s", peer), e);
}
}
private void onNetworkChatMessagesMessage(Peer peer, Message message) {
ChatMessagesMessage chatMessagesMessage = (ChatMessagesMessage) message;
final List<ChatMessage> chatMessages = chatMessagesMessage.getChatMessages();
try (final Repository chatRepository = ChatRepositoryManager.getRepository()) {
List<ChatMessage> newChatMessages = new ArrayList<>();
for (ChatMessage chatMessage : chatMessages) {
// Check if we already have this message
ChatMessage existingChatMessage = chatRepository.getChatRepository().getChatMessageBySignature(chatMessage.getSignature());
if (existingChatMessage != null) {
continue;
}
newChatMessages.add(chatMessage);
chatRepository.getChatRepository().save(chatMessage);
}
if (!newChatMessages.isEmpty()) {
// Notify other peers about new message(s)
onNewChatMessages(newChatMessages);
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending recent chat messages to peer %s", peer), e);
}
}
private void onNetworkChatMessageSignaturesMessage(Peer peer, Message message) {
ChatMessageSignaturesMessage chatMessageSignaturesMessage = (ChatMessageSignaturesMessage) message;
final List<byte[]> signatures = chatMessageSignaturesMessage.getSignatures();
try (final Repository chatRepository = ChatRepositoryManager.getRepository()) {
for (byte[] signature : signatures) {
// Check if we already have this message
ChatMessage existingChatMessage = chatRepository.getChatRepository().getChatMessageBySignature(signature);
if (existingChatMessage != null) {
continue;
}
// Request the message itself
Message getChatMessageMessage = new GetChatMessageMessage(signature);
if (!peer.sendMessage(getChatMessageMessage)) {
peer.disconnect("failed to request chat message");
return;
}
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending recent chat messages to peer %s", peer), e);
}
}
// Utilities

View File

@@ -48,7 +48,6 @@ public class ArbitraryDataStorageManager extends Thread {
private List<ArbitraryTransactionData> hostedTransactions;
private String searchQuery;
private List<ArbitraryTransactionData> searchResultsTransactions;
private static final long DIRECTORY_SIZE_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 minutes
@@ -344,11 +343,6 @@ public class ArbitraryDataStorageManager extends Thread {
*/
public List<ArbitraryTransactionData> searchHostedTransactions(Repository repository, String query, Integer limit, Integer offset) {
// Load from results cache if we can (results that exists for the same query), to avoid disk reads
if (this.searchResultsTransactions != null && this.searchQuery.equals(query.toLowerCase())) {
return ArbitraryTransactionUtils.limitOffsetTransactions(this.searchResultsTransactions, limit, offset);
}
// Using cache if we can, to avoid disk reads
if (this.hostedTransactions == null) {
this.hostedTransactions = this.loadAllHostedTransactions(repository);
@@ -376,10 +370,7 @@ public class ArbitraryDataStorageManager extends Thread {
// Sort by newest first
searchResultsList.sort(Comparator.comparingLong(ArbitraryTransactionData::getTimestamp).reversed());
// Update cache
this.searchResultsTransactions = searchResultsList;
return ArbitraryTransactionUtils.limitOffsetTransactions(this.searchResultsTransactions, limit, offset);
return ArbitraryTransactionUtils.limitOffsetTransactions(searchResultsList, limit, offset);
}
/**

View File

@@ -39,9 +39,10 @@ public class AtStatesPruner implements Runnable {
try (final Repository repository = RepositoryManager.getRepository()) {
int pruneStartHeight = repository.getATRepository().getAtPruneHeight();
int maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
repository.discardChanges();
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
repository.saveChanges();
while (!Controller.isStopping()) {
@@ -92,7 +93,8 @@ public class AtStatesPruner implements Runnable {
if (upperPrunableHeight > upperBatchHeight) {
pruneStartHeight = upperBatchHeight;
repository.getATRepository().setAtPruneHeight(pruneStartHeight);
repository.getATRepository().rebuildLatestAtStates();
maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
repository.saveChanges();
final int finalPruneStartHeight = pruneStartHeight;

View File

@@ -26,9 +26,10 @@ public class AtStatesTrimmer implements Runnable {
try (final Repository repository = RepositoryManager.getRepository()) {
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
int maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
repository.discardChanges();
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
repository.saveChanges();
while (!Controller.isStopping()) {
@@ -70,7 +71,8 @@ public class AtStatesTrimmer implements Runnable {
if (upperTrimmableHeight > upperBatchHeight) {
trimStartHeight = upperBatchHeight;
repository.getATRepository().setAtTrimHeight(trimStartHeight);
repository.getATRepository().rebuildLatestAtStates();
maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
repository.saveChanges();
final int finalTrimStartHeight = trimStartHeight;

View File

@@ -102,6 +102,21 @@ public class NamesDatabaseIntegrityCheck {
}
}
// Process CANCEL_SELL_NAME transactions
if (currentTransaction.getType() == TransactionType.CANCEL_SELL_NAME) {
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) currentTransaction;
Name nameObj = new Name(repository, cancelSellNameTransactionData.getName());
if (nameObj != null && nameObj.getNameData() != null) {
nameObj.cancelSell(cancelSellNameTransactionData);
modificationCount++;
LOGGER.trace("Processed CANCEL_SELL_NAME transaction for name {}", name);
}
else {
// Something went wrong
throw new DataException(String.format("Name data not found for name %s", cancelSellNameTransactionData.getName()));
}
}
// Process BUY_NAME transactions
if (currentTransaction.getType() == TransactionType.BUY_NAME) {
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) currentTransaction;
@@ -128,7 +143,7 @@ public class NamesDatabaseIntegrityCheck {
public int rebuildAllNames() {
int modificationCount = 0;
try (final Repository repository = RepositoryManager.getRepository()) {
List<String> names = this.fetchAllNames(repository);
List<String> names = this.fetchAllNames(repository); // TODO: de-duplicate, to speed up this process
for (String name : names) {
modificationCount += this.rebuildName(name, repository);
}
@@ -326,6 +341,10 @@ public class NamesDatabaseIntegrityCheck {
TransactionType.BUY_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
signatures.addAll(buyNameTransactions);
List<byte[]> cancelSellNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
TransactionType.CANCEL_SELL_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
signatures.addAll(cancelSellNameTransactions);
List<TransactionData> transactions = new ArrayList<>();
for (byte[] signature : signatures) {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
@@ -390,6 +409,12 @@ public class NamesDatabaseIntegrityCheck {
names.add(sellNameTransactionData.getName());
}
}
if ((transactionData instanceof CancelSellNameTransactionData)) {
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData;
if (!names.contains(cancelSellNameTransactionData.getName())) {
names.add(cancelSellNameTransactionData.getName());
}
}
}
return names;
}

View File

@@ -157,4 +157,18 @@ public class PruneManager {
return (height < latestUnprunedHeight);
}
/**
* When rebuilding the latest AT states, we need to specify a maxHeight, so that we aren't tracking
* very recent AT states that could potentially be orphaned. This method ensures that AT states
* are given a sufficient number of blocks to confirm before being tracked as a latest AT state.
*/
public static int getMaxHeightForLatestAtStates(Repository repository) throws DataException {
// Get current chain height, and subtract a certain number of "confirmation" blocks
// This is to ensure we are basing our latest AT states data on confirmed blocks -
// ones that won't be orphaned in any normal circumstances
final int confirmationBlocks = 250;
final int chainHeight = repository.getBlockRepository().getBlockchainHeight();
return chainHeight - confirmationBlocks;
}
}

View File

@@ -49,6 +49,7 @@ public class Bitcoin extends Bitcoiny {
//CLOSED new Server("bitcoin.grey.pw", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("btc.litepay.ch", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrum.pabu.io", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrumx.dev", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrumx.hodlwallet.com", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("gd42.org", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("korea.electrum-server.com", Server.ConnectionType.SSL, 50002),
@@ -56,28 +57,75 @@ public class Bitcoin extends Bitcoiny {
//1.15.0 new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002),
//1.15.0 new Server("electrum.acinq.co", Server.ConnectionType.SSL, 50002),
//1.14.0 new Server("electrum.coinext.com.br", Server.ConnectionType.SSL, 50002),
//F1.7.0 new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002),
new Server("104.248.139.211", Server.ConnectionType.SSL, 50002),
new Server("128.0.190.26", Server.ConnectionType.SSL, 50002),
new Server("142.93.6.38", Server.ConnectionType.SSL, 50002),
new Server("157.245.172.236", Server.ConnectionType.SSL, 50002),
new Server("167.172.226.175", Server.ConnectionType.SSL, 50002),
new Server("167.172.42.31", Server.ConnectionType.SSL, 50002),
new Server("178.62.80.20", Server.ConnectionType.SSL, 50002),
new Server("185.64.116.15", Server.ConnectionType.SSL, 50002),
new Server("188.165.206.215", Server.ConnectionType.SSL, 50002),
new Server("188.165.211.112", Server.ConnectionType.SSL, 50002),
new Server("2azzarita.hopto.org", Server.ConnectionType.SSL, 50002),
new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022),
new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
new Server("65.39.140.37", Server.ConnectionType.SSL, 50002),
new Server("68.183.188.105", Server.ConnectionType.SSL, 50002),
new Server("71.73.14.254", Server.ConnectionType.SSL, 50002),
new Server("94.23.247.135", Server.ConnectionType.SSL, 50002),
new Server("assuredly.not.fyi", Server.ConnectionType.SSL, 50002),
new Server("ax101.blockeng.ch", Server.ConnectionType.SSL, 50002),
new Server("ax102.blockeng.ch", Server.ConnectionType.SSL, 50002),
new Server("b.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("b6.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.dermichi.com", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.lu.ke", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.lukechilds.co", Server.ConnectionType.SSL, 50002),
new Server("blkhub.net", Server.ConnectionType.SSL, 50002),
new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002),
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
new Server("btc.ocf.sh", Server.ConnectionType.SSL, 50002),
new Server("btce.iiiiiii.biz", Server.ConnectionType.SSL, 50002),
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002),
new Server("eai.coincited.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.bhoovd.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002),
new Server("electrumx.dev", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitcoinlizard.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002),
new Server("electrum.exan.tech", Server.ConnectionType.SSL, 50002),
new Server("electrum.kendigisland.xyz", Server.ConnectionType.SSL, 50002),
new Server("electrum.mmitech.info", Server.ConnectionType.SSL, 50002),
new Server("electrum.petrkr.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.thomasfischbach.de", Server.ConnectionType.SSL, 50002),
new Server("electrum0.snel.it", Server.ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002),
new Server("electrumx-core.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("elx.bitske.com", Server.ConnectionType.SSL, 50002),
new Server("ex03.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("ex05.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("ex07.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("fortress.qtornado.com", Server.ConnectionType.SSL, 50002),
new Server("fulcrum.grey.pw", Server.ConnectionType.SSL, 50002),
new Server("fulcrum.sethforprivacy.com", Server.ConnectionType.SSL, 51002),
new Server("guichet.centure.cc", Server.ConnectionType.SSL, 50002),
new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002),
new Server("hodlers.beer", Server.ConnectionType.SSL, 50002),
new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002),
new Server("kirsche.emzy.de", Server.ConnectionType.SSL, 50002),
new Server("node1.btccuracao.com", Server.ConnectionType.SSL, 50002),
new Server("osr1ex1.compumundohipermegared.one", Server.ConnectionType.SSL, 50002),
new Server("smmalis37.ddns.net", Server.ConnectionType.SSL, 50002),
new Server("ulrichard.ch", Server.ConnectionType.SSL, 50002),
new Server("vmd104012.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd104014.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd63185.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd71287.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd84592.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("xtrum.com", Server.ConnectionType.SSL, 50002));
}

View File

@@ -167,6 +167,16 @@ public abstract class Bitcoiny implements ForeignBlockchain {
return blockTimestamps.get(5);
}
/**
* Returns height from latest block.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
public int getBlockchainHeight() throws ForeignBlockchainException {
int height = this.blockchainProvider.getCurrentHeight();
return height;
}
/** Returns fee per transaction KB. To be overridden for testnet/regtest. */
public Coin getFeePerKb() {
return this.bitcoinjContext.getFeePerKb();

View File

@@ -45,6 +45,8 @@ public class Digibyte extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb
new Server("electrum-dgb.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1-dgb.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", ConnectionType.SSL, 20059),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20059),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20059));

View File

@@ -45,10 +45,12 @@ public class Dogecoin extends Bitcoiny {
public Collection<Server> getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=doge
new Server("electrum-doge.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1-doge.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", ConnectionType.SSL, 20060),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20060),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20060));
// TODO: add more mainnet servers. It's too centralized.
}
@Override

View File

@@ -5,6 +5,7 @@ import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.text.DecimalFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,7 +31,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
private static final Random RANDOM = new Random();
// See: https://electrumx.readthedocs.io/en/latest/protocol-changes.html
private static final double MIN_PROTOCOL_VERSION = 1.2;
private static final double MAX_PROTOCOL_VERSION = 2.0; // Higher than current latest, for hopeful future-proofing
private static final String CLIENT_NAME = "Qortal";
private static final int BLOCK_HEADER_LENGTH = 80;
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
@@ -679,6 +684,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
this.scanner = new Scanner(this.socket.getInputStream());
this.scanner.useDelimiter("\n");
// All connections need to start with a version negotiation
this.connectedRpc("server.version");
// Check connection is suitable by asking for server features, including genesis block hash
JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features");
@@ -725,6 +733,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
JSONArray requestParams = new JSONArray();
requestParams.addAll(Arrays.asList(params));
// server.version needs additional params to negotiate a version
if (method.equals("server.version")) {
requestParams.add(CLIENT_NAME);
List<String> versions = new ArrayList<>();
DecimalFormat df = new DecimalFormat("#.#");
versions.add(df.format(MIN_PROTOCOL_VERSION));
versions.add(df.format(MAX_PROTOCOL_VERSION));
requestParams.add(versions);
}
requestJson.put("params", requestParams);
String request = requestJson.toJSONString() + "\n";

View File

@@ -45,15 +45,18 @@ public class Litecoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=ltc
//CLOSED new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
//CLOSED new Server("electrum-ltc.someguy123.net", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
//BEHIND new Server("62.171.169.176", Server.ConnectionType.SSL, 50002),
//PHISHY new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 50002),
new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
new Server("electrum-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
new Server("ltc.rentonrisk.com", Server.ConnectionType.SSL, 50002));
}

View File

@@ -57,9 +57,9 @@ public class PirateChain extends Bitcoiny {
public Collection<Server> getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("arrrlightd.qortal.online", ConnectionType.SSL, 443),
new Server("arrrlightd1.qortal.online", ConnectionType.SSL, 443),
new Server("arrrlightd2.qortal.online", ConnectionType.SSL, 443),
new Server("wallet-arrr1.qortal.online", ConnectionType.SSL, 443),
new Server("wallet-arrr2.qortal.online", ConnectionType.SSL, 443),
new Server("wallet-arrr3.qortal.online", ConnectionType.SSL, 443),
new Server("lightd.pirate.black", ConnectionType.SSL, 443));
}

View File

@@ -45,13 +45,16 @@ public class Ravencoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn
new Server("aethyn.com", ConnectionType.SSL, 50002),
new Server("electrum2.rvn.rocks", ConnectionType.SSL, 50002),
new Server("rvn-dashboard.com", ConnectionType.SSL, 50002),
new Server("rvn4lyfe.com", ConnectionType.SSL, 50002),
//CLOSED new Server("aethyn.com", ConnectionType.SSL, 50002),
//CLOSED new Server("electrum2.rvn.rocks", ConnectionType.SSL, 50002),
//BEHIND new Server("electrum3.rvn.rocks", ConnectionType.SSL, 50002),
new Server("electrum-rvn.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1-rvn.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", ConnectionType.SSL, 20051),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20051),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20051));
new Server("electrum3.cipig.net", ConnectionType.SSL, 20051),
new Server("rvn-dashboard.com", ConnectionType.SSL, 50002),
new Server("rvn4lyfe.com", ConnectionType.SSL, 50002));
}
@Override

View File

@@ -0,0 +1,100 @@
package org.qortal.data.network;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
import org.qortal.data.transaction.TransactionData;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class ChatMessageData {
// Properties
@Schema(description = "timestamp when message created, in milliseconds since unix epoch", example = "__unix_epoch_time_milliseconds__")
protected long timestamp;
@Schema(description = "groupID for this message")
protected int txGroupId; // TODO: rename?
@Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
private byte[] senderPublicKey;
@Schema(accessMode = AccessMode.READ_ONLY)
private String sender;
@Schema(accessMode = AccessMode.READ_ONLY)
private int nonce;
private String recipient; // can be null
@Schema(description = "raw message data, possibly UTF8 text", example = "2yGEbwRFyhPZZckKA")
private byte[] data;
private boolean isText;
private boolean isEncrypted;
// Constructors
// For JAXB
protected ChatMessageData() {}
public ChatMessageData(long timestamp, int txGroupId, byte[] senderPublicKey,
String sender, int nonce, String recipient, byte[] data, boolean isText, boolean isEncrypted) {
this.timestamp = timestamp;
this.txGroupId = txGroupId;
this.senderPublicKey = senderPublicKey;
this.sender = sender;
this.nonce = nonce;
this.recipient = recipient;
this.data = data;
this.isText = isText;
this.isEncrypted = isEncrypted;
}
// Getters/Setters
public long getTimestamp() {
return this.timestamp;
}
public int getTxGroupId() {
return this.txGroupId;
}
public byte[] getSenderPublicKey() {
return this.senderPublicKey;
}
public String getSender() {
return this.sender;
}
public int getNonce() {
return this.nonce;
}
public void setNonce(int nonce) {
this.nonce = nonce;
}
public String getRecipient() {
return this.recipient;
}
public byte[] getData() {
return this.data;
}
public boolean getIsText() {
return this.isText;
}
public boolean getIsEncrypted() {
return this.isEncrypted;
}
}

View File

@@ -3,6 +3,7 @@ package org.qortal.data.transaction;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlTransient;
import org.qortal.transaction.Transaction.TransactionType;
@@ -19,6 +20,11 @@ public class CancelSellNameTransactionData extends TransactionData {
@Schema(description = "which name to cancel selling", example = "my-name")
private String name;
// For internal use when orphaning
@XmlTransient
@Schema(hidden = true)
private Long salePrice;
// Constructors
// For JAXB
@@ -30,11 +36,17 @@ public class CancelSellNameTransactionData extends TransactionData {
this.creatorPublicKey = this.ownerPublicKey;
}
public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name) {
public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name, Long salePrice) {
super(TransactionType.CANCEL_SELL_NAME, baseTransactionData);
this.ownerPublicKey = baseTransactionData.creatorPublicKey;
this.name = name;
this.salePrice = salePrice;
}
/** From network/API */
public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name) {
this(baseTransactionData, name, null);
}
// Getters / setters
@@ -47,4 +59,12 @@ public class CancelSellNameTransactionData extends TransactionData {
return this.name;
}
public Long getSalePrice() {
return this.salePrice;
}
public void setSalePrice(Long salePrice) {
this.salePrice = salePrice;
}
}

View File

@@ -180,8 +180,12 @@ public class Name {
}
public void cancelSell(CancelSellNameTransactionData cancelSellNameTransactionData) throws DataException {
// Mark not for-sale but leave price in case we want to orphan
// Update previous sale price in transaction data
cancelSellNameTransactionData.setSalePrice(this.nameData.getSalePrice());
// Mark not for-sale
this.nameData.setIsForSale(false);
this.nameData.setSalePrice(null);
// Save sale info into repository
this.repository.getNameRepository().save(this.nameData);
@@ -190,6 +194,7 @@ public class Name {
public void uncancelSell(CancelSellNameTransactionData cancelSellNameTransactionData) throws DataException {
// Mark as for-sale using existing price
this.nameData.setIsForSale(true);
this.nameData.setSalePrice(cancelSellNameTransactionData.getSalePrice());
// Save no-sale info into repository
this.repository.getNameRepository().save(this.nameData);

View File

@@ -12,6 +12,7 @@ import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.crypto.Crypto;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.message.*;
@@ -20,11 +21,8 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.*;
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
import org.qortal.utils.NTP;
import org.qortal.utils.NamedThreadFactory;
import java.io.IOException;
import java.net.InetAddress;
@@ -88,6 +86,8 @@ public class Network {
public static final int MAX_SIGNATURES_PER_REPLY = 500;
public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500;
public static final int MAX_CHAT_MESSAGES_PER_REPLY = 500;
public static final int MAX_CHAT_MESSAGE_SIGNATURES_PER_REQUEST = 10000; // 640 KiB
private static final long DISCONNECTION_CHECK_INTERVAL = 10 * 1000L; // milliseconds
@@ -339,7 +339,7 @@ public class Network {
try {
if (!isConnected) {
// Add this signature to the list of pending requests for this peer
LOGGER.info("Making connection to peer {} to request files for signature {}...", peerAddressString, Base58.encode(signature));
LOGGER.debug("Making connection to peer {} to request files for signature {}...", peerAddressString, Base58.encode(signature));
Peer peer = new Peer(peerData);
peer.setIsDataPeer(true);
peer.addPendingSignatureRequest(signature);
@@ -1225,6 +1225,29 @@ public class Network {
return new GetUnconfirmedTransactionsMessage();
}
public Message buildChatMessageSignaturesMessage() {
try (final Repository repository = RepositoryManager.getRepository()) {
// Don't request further back than 24 hours
long after = NTP.getTime() - (24 * 60 * 60 * 1000L);
// Get all recent messages from repository
List<ChatMessage> ourChatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
null, after, null, null, null, null, null, null, 0, true);
List<byte[]> signatures = new ArrayList<>();
for (ChatMessage chatMessage : ourChatMessages) {
signatures.add(chatMessage.getSignature());
}
return new ChatMessageSignaturesMessage(signatures);
} catch (DataException e) {
LOGGER.error("Repository issue while building recent chat messages message", e);
}
return null;
}
// External IP / peerAddress tracking

View File

@@ -0,0 +1,63 @@
package org.qortal.network.message;
import com.google.common.primitives.Ints;
import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class ChatMessageSignaturesMessage extends Message {
public static final long MIN_PEER_VERSION = 0x300040000L; // 3.4.0
private List<byte[]> signatures;
public ChatMessageSignaturesMessage(List<byte[]> signatures) {
super(MessageType.CHAT_MESSAGE_SIGNATURES);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(signatures.size()));
for (byte[] signature : signatures)
bytes.write(signature);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private ChatMessageSignaturesMessage(int id, List<byte[]> signatures) {
super(id, MessageType.CHAT_MESSAGE_SIGNATURES);
this.signatures = signatures;
}
public List<byte[]> getSignatures() {
return this.signatures;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int count = bytes.getInt();
if (bytes.remaining() < count * Transformer.SIGNATURE_LENGTH)
throw new BufferUnderflowException();
List<byte[]> signatures = new ArrayList<>();
for (int i = 0; i < count; ++i) {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
signatures.add(signature);
}
return new ChatMessageSignaturesMessage(id, signatures);
}
}

View File

@@ -0,0 +1,141 @@
package org.qortal.network.message;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import org.qortal.data.chat.ChatMessage;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
import org.qortal.utils.Serialization;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static org.qortal.naming.Name.MAX_NAME_SIZE;
import static org.qortal.transform.Transformer.SIGNATURE_LENGTH;
public class ChatMessagesMessage extends Message {
private List<ChatMessage> chatMessages;
private static final int CHAT_REFERENCE_LENGTH = SIGNATURE_LENGTH;
public ChatMessagesMessage(List<ChatMessage> chatMessages) {
super(MessageType.CHAT_MESSAGES);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(chatMessages.size()));
for (ChatMessage chatMessage : chatMessages) {
bytes.write(Longs.toByteArray(chatMessage.getTimestamp()));
bytes.write(Ints.toByteArray(chatMessage.getTxGroupId()));
bytes.write(chatMessage.getReference());
bytes.write(chatMessage.getSenderPublicKey());
Serialization.serializeSizedStringV2(bytes, chatMessage.getSender());
Serialization.serializeSizedStringV2(bytes, chatMessage.getSenderName());
Serialization.serializeSizedStringV2(bytes, chatMessage.getRecipient());
Serialization.serializeSizedStringV2(bytes, chatMessage.getRecipientName());
// Include chat reference if it's not null
if (chatMessage.getChatReference() != null) {
bytes.write((byte) 1);
bytes.write(chatMessage.getChatReference());
} else {
bytes.write((byte) 0);
}
bytes.write(Ints.toByteArray(chatMessage.getData().length));
bytes.write(chatMessage.getData());
bytes.write(Ints.toByteArray(chatMessage.isText() ? 1 : 0));
bytes.write(Ints.toByteArray(chatMessage.isEncrypted() ? 1 : 0));
bytes.write(chatMessage.getSignature());
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private ChatMessagesMessage(int id, List<ChatMessage> chatMessages) {
super(id, MessageType.CHAT_MESSAGES);
this.chatMessages = chatMessages;
}
public List<ChatMessage> getChatMessages() {
return this.chatMessages;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
try {
int count = bytes.getInt();
List<ChatMessage> chatMessages = new ArrayList<>();
for (int i = 0; i < count; ++i) {
long timestamp = bytes.getLong();
int txGroupId = bytes.getInt();
byte[] reference = new byte[SIGNATURE_LENGTH];
bytes.get(reference);
byte[] senderPublicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
bytes.get(senderPublicKey);
String sender = Serialization.deserializeSizedStringV2(bytes, Transformer.BASE58_ADDRESS_LENGTH);
String senderName = Serialization.deserializeSizedStringV2(bytes, MAX_NAME_SIZE);
String recipient = Serialization.deserializeSizedStringV2(bytes, Transformer.BASE58_ADDRESS_LENGTH);
String recipientName = Serialization.deserializeSizedStringV2(bytes, MAX_NAME_SIZE);
byte[] chatReference = null;
boolean hasChatReference = bytes.get() != 0;
if (hasChatReference) {
chatReference = new byte[CHAT_REFERENCE_LENGTH];
bytes.get(chatReference);
}
int dataLength = bytes.getInt();
byte[] data = new byte[dataLength];
bytes.get(data);
boolean isText = bytes.getInt() == 1;
boolean isEncrypted = bytes.getInt() == 1;
byte[] signature = new byte[SIGNATURE_LENGTH];
bytes.get(signature);
ChatMessage chatMessage = new ChatMessage(timestamp, txGroupId, reference, senderPublicKey,
sender, senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signature);
chatMessages.add(chatMessage);
}
return new ChatMessagesMessage(id, chatMessages);
} catch (TransformationException e) {
throw new MessageException(e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,46 @@
package org.qortal.network.message;
import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class GetChatMessageMessage extends Message {
private byte[] signature;
public GetChatMessageMessage(byte[] signature) {
super(MessageType.GET_CHAT_MESSAGE);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(signature);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetChatMessageMessage(int id, byte[] signature) {
super(id, MessageType.GET_CHAT_MESSAGE);
this.signature = signature;
}
public byte[] getSignature() {
return this.signature;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
return new GetChatMessageMessage(id, signature);
}
}

View File

@@ -0,0 +1,87 @@
package org.qortal.network.message;
import com.google.common.primitives.Ints;
import org.qortal.transaction.Transaction;
import org.qortal.transform.block.BlockTransformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
public class GetChatMessagesMessage extends Message {
public enum Direction {
FORWARDS(0),
BACKWARDS(1);
public final int value;
private static final Map<Integer, Direction> map = stream(Direction.values()).collect(toMap(result -> result.value, result -> result));
Direction(int value) {
this.value = value;
}
public static Direction valueOf(int value) {
return map.get(value);
}
}
private long timestamp;
private int numberRequested;
private Direction direction;
public GetChatMessagesMessage(byte[] referenceSignature, int numberRequested, Direction direction) {
super(MessageType.GET_CHAT_MESSAGES);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(referenceSignature);
bytes.write(Ints.toByteArray(numberRequested));
bytes.write(Ints.toByteArray(direction.value));
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetChatMessagesMessage(int id, long timestamp, int numberRequested, Direction direction) {
super(id, MessageType.GET_CHAT_MESSAGES);
this.timestamp = timestamp;
this.numberRequested = numberRequested;
this.direction = direction;
}
public long getTimestamp() {
return this.timestamp;
}
public int getNumberRequested() {
return this.numberRequested;
}
public Direction getDirection() {
return this.direction;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
long timestamp = bytes.getLong();
int numberRequested = bytes.getInt();
Direction direction = Direction.valueOf(bytes.getInt());
return new GetChatMessagesMessage(id, timestamp, numberRequested, direction);
}
}

View File

@@ -0,0 +1,60 @@
package org.qortal.network.message;
import com.google.common.primitives.Ints;
import org.qortal.transform.Transformer;
import org.qortal.utils.ByteArray;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class GetRecentChatMessagesMessage extends Message {
private List<ByteArray> signatures;
public GetRecentChatMessagesMessage(List<ByteArray> signatures) {
super(MessageType.GET_RECENT_CHAT_MESSAGES);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(signatures.size()));
for (ByteArray signature : signatures) {
bytes.write(signature.value);
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetRecentChatMessagesMessage(int id, List<ByteArray> signatures) {
super(id, MessageType.GET_RECENT_CHAT_MESSAGES);
this.signatures = signatures;
}
public List<ByteArray> getSignatures() {
return this.signatures;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int count = bytes.getInt();
List<ByteArray> signatures = new ArrayList<>();
for (int i = 0; i < count; ++i) {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
signatures.add(ByteArray.wrap(signature));
}
return new GetRecentChatMessagesMessage(id, signatures);
}
}

View File

@@ -83,6 +83,10 @@ public abstract class Message {
return this.type;
}
public byte[] getDataBytes() {
return this.dataBytes;
}
/**
* Attempt to read a message from byte buffer.
*

View File

@@ -83,7 +83,15 @@ public enum MessageType {
GET_NAME(182, GetNameMessage::fromByteBuffer),
TRANSACTIONS(190, TransactionsMessage::fromByteBuffer),
GET_ACCOUNT_TRANSACTIONS(191, GetAccountTransactionsMessage::fromByteBuffer);
GET_ACCOUNT_TRANSACTIONS(191, GetAccountTransactionsMessage::fromByteBuffer),
// Chat messages
CHAT_MESSAGE_SIGNATURES(200, ChatMessageSignaturesMessage::fromByteBuffer),
CHAT_MESSAGES(201, TransactionsMessage::fromByteBuffer),
GET_CHAT_MESSAGES(202, GetChatMessagesMessage::fromByteBuffer),
GET_CHAT_MESSAGE(203, GetChatMessageMessage::fromByteBuffer),
GET_RECENT_CHAT_MESSAGES(204, GetRecentChatMessagesMessage::fromByteBuffer);
public final int value;
public final MessageProducer fromByteBufferMethod;

View File

@@ -119,7 +119,7 @@ public interface ATRepository {
* <p>
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
*/
public void rebuildLatestAtStates() throws DataException;
public void rebuildLatestAtStates(int maxHeight) throws DataException;
/** Returns height of first trimmable AT state. */

View File

@@ -10,15 +10,15 @@ public interface ChatRepository {
/**
* Returns CHAT messages matching criteria.
* <p>
* Expects EITHER non-null txGroupID OR non-null sender and recipient addresses.
*/
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after,
Integer txGroupId, byte[] reference, byte[] chatReferenceBytes, Boolean hasChatReference,
List<String> involving, Integer limit, Integer offset, Boolean reverse) throws DataException;
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException;
public ActiveChats getActiveChats(String address) throws DataException;
public ChatMessage getChatMessageBySignature(byte[] signature) throws DataException;
public void save(ChatMessage chatMessage) throws DataException;
}

View File

@@ -0,0 +1,69 @@
package org.qortal.repository;
import java.util.concurrent.TimeoutException;
// TODO: extend RepositoryManager, but only after moving away from static methods
public class ChatRepositoryManager {
private static RepositoryFactory repositoryFactory = null;
/** null if no checkpoint requested, TRUE for quick checkpoint, false for slow/full checkpoint. */
private static Boolean quickCheckpointRequested = 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 chat repository available");
return repositoryFactory.wasPristineAtOpen();
}
public static Repository getRepository() throws DataException {
if (repositoryFactory == null)
throw new DataException("No chat repository available");
return repositoryFactory.getRepository();
}
public static Repository tryRepository() throws DataException {
if (repositoryFactory == null)
throw new DataException("No chat repository available");
return repositoryFactory.tryRepository();
}
public static void closeRepositoryFactory() throws DataException {
repositoryFactory.close();
repositoryFactory = null;
}
public static void backup(boolean quick, String name, Long timeout) throws TimeoutException {
// Backups currently unsupported for chat repository
}
public static boolean archive(Repository repository) {
// Archiving not supported
return false;
}
public static boolean prune(Repository repository) {
// Pruning not supported
return false;
}
public static void setRequestedCheckpoint(Boolean quick) {
quickCheckpointRequested = quick;
}
public static Boolean getRequestedCheckpoint() {
return quickCheckpointRequested;
}
}

View File

@@ -603,7 +603,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public void rebuildLatestAtStates() throws DataException {
public void rebuildLatestAtStates(int maxHeight) throws DataException {
// latestATStatesLock is to prevent concurrent updates on LatestATStates
// that could result in one process using a partial or empty dataset
// because it was in the process of being rebuilt by another thread
@@ -624,11 +624,12 @@ public class HSQLDBATRepository implements ATRepository {
+ "CROSS JOIN LATERAL("
+ "SELECT height FROM ATStates "
+ "WHERE ATStates.AT_address = ATs.AT_address "
+ "AND height <= ?"
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
+ ") "
+ ")";
try {
this.repository.executeCheckedUpdate(insertSql);
this.repository.executeCheckedUpdate(insertSql, maxHeight);
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to populate temporary latest AT states cache in repository", e);

View File

@@ -0,0 +1,133 @@
package org.qortal.repository.hsqldb;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.controller.Controller;
import org.qortal.gui.SplashFrame;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
// TODO: extend HSQLDBDatabaseUpdates, but only after moving away from static methods
public class HSQLDBChatDatabaseUpdates {
private static final Logger LOGGER = LogManager.getLogger(HSQLDBChatDatabaseUpdates.class);
/**
* Apply any incremental changes to database schema.
*
* @return true if database was non-existent/empty, false otherwise
* @throws SQLException
*/
public static boolean updateDatabase(Connection connection) throws SQLException {
final boolean wasPristine = fetchDatabaseVersion(connection) == 0;
SplashFrame.getInstance().updateStatus("Upgrading chat database, please wait...");
while (databaseUpdating(connection, wasPristine))
incrementDatabaseVersion(connection);
String text = String.format("Starting Qortal Core v%s...", Controller.getInstance().getVersionStringWithoutPrefix());
SplashFrame.getInstance().updateStatus(text);
return wasPristine;
}
/**
* Increment database's schema version.
*
* @throws SQLException
*/
private static void incrementDatabaseVersion(Connection connection) throws SQLException {
try (Statement stmt = connection.createStatement()) {
stmt.execute("UPDATE DatabaseInfo SET version = version + 1");
connection.commit();
}
}
/**
* Fetch current version of database schema.
*
* @return database version, or 0 if no schema yet
* @throws SQLException
*/
protected static int fetchDatabaseVersion(Connection connection) throws SQLException {
try (Statement stmt = connection.createStatement()) {
if (stmt.execute("SELECT version FROM DatabaseInfo"))
try (ResultSet resultSet = stmt.getResultSet()) {
if (resultSet.next())
return resultSet.getInt(1);
}
} catch (SQLException e) {
// empty database
}
return 0;
}
/**
* Incrementally update database schema, returning whether an update happened.
*
* @return true - if a schema update happened, false otherwise
* @throws SQLException
*/
private static boolean databaseUpdating(Connection connection, boolean wasPristine) throws SQLException {
int databaseVersion = fetchDatabaseVersion(connection);
try (Statement stmt = connection.createStatement()) {
switch (databaseVersion) {
case 0:
// create from new
// FYI: "UCC" in HSQLDB means "upper-case comparison", i.e. case-insensitive
stmt.execute("SET DATABASE SQL NAMES TRUE"); // SQL keywords cannot be used as DB object names, e.g. table names
stmt.execute("SET DATABASE SQL SYNTAX MYS TRUE"); // Required for our use of INSERT ... ON DUPLICATE KEY UPDATE ... syntax
stmt.execute("SET DATABASE SQL RESTRICT EXEC TRUE"); // No multiple-statement execute() or DDL/DML executeQuery()
stmt.execute("SET DATABASE TRANSACTION CONTROL MVCC"); // Use MVCC over default two-phase locking, a-k-a "LOCKS"
stmt.execute("SET DATABASE DEFAULT TABLE TYPE CACHED");
stmt.execute("SET DATABASE COLLATION SQL_TEXT NO PAD"); // Do not pad strings to same length before comparison
stmt.execute("CREATE COLLATION SQL_TEXT_UCC_NO_PAD FOR SQL_TEXT FROM SQL_TEXT_UCC NO PAD");
stmt.execute("CREATE COLLATION SQL_TEXT_NO_PAD FOR SQL_TEXT FROM SQL_TEXT NO PAD");
stmt.execute("SET FILES SPACE TRUE"); // Enable per-table block space within .data file, useful for CACHED table types
// Slow down log fsync() calls from every 500ms to reduce I/O load
stmt.execute("SET FILES WRITE DELAY 5"); // only fsync() every 5 seconds
stmt.execute("CREATE TABLE DatabaseInfo ( version INTEGER NOT NULL )");
stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )");
stmt.execute("CREATE TYPE Signature AS VARBINARY(64)");
stmt.execute("CREATE TYPE QortalAddress AS VARCHAR(36)");
stmt.execute("CREATE TYPE MessageData AS VARBINARY(4000)");
break;
case 1:
// Chat messages
stmt.execute("CREATE TABLE ChatMessages (signature Signature, reference Signature,"
+ "created_when EpochMillis NOT NULL, tx_group_id GroupID NOT NULL, "
+ "sender QortalAddress NOT NULL, nonce INT NOT NULL, recipient QortalAddress, "
+ "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, data MessageData NOT NULL, "
+ "PRIMARY KEY (signature))");
// For finding chat messages by sender
stmt.execute("CREATE INDEX ChatMessagesSenderIndex ON ChatMessages (sender)");
// For finding chat messages by recipient
stmt.execute("CREATE INDEX ChatMessagesRecipientIndex ON ChatMessages (recipient, sender)");
break;
default:
// nothing to do
return false;
}
}
// database was updated
LOGGER.info(() -> String.format("HSQLDB chat repository updated to version %d", databaseVersion + 1));
return true;
}
}

View File

@@ -5,11 +5,11 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.qortal.data.asset.OrderData;
import org.qortal.data.chat.ActiveChats;
import org.qortal.data.chat.ActiveChats.DirectChat;
import org.qortal.data.chat.ActiveChats.GroupChat;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.repository.ChatRepository;
import org.qortal.repository.DataException;
import org.qortal.transaction.Transaction.TransactionType;
@@ -33,13 +33,12 @@ public class HSQLDBChatRepository implements ChatRepository {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT created_when, tx_group_id, Transactions.reference, creator, "
+ "sender, SenderNames.name, recipient, RecipientNames.name, "
sql.append("SELECT created_when, tx_group_id, Transactions.reference, creator, sender, recipient, "
// TODO: + "SenderNames.name, RecipientNames.name, "
+ "chat_reference, data, is_text, is_encrypted, signature "
+ "FROM ChatTransactions "
+ "JOIN Transactions USING (signature) "
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
+ "LEFT OUTER JOIN Names AS RecipientNames ON RecipientNames.owner = recipient ");
+ "FROM ChatMessages ");
// TODO: + "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
// TODO: + "LEFT OUTER JOIN Names AS RecipientNames ON RecipientNames.owner = recipient ");
// WHERE clauses
@@ -95,7 +94,7 @@ public class HSQLDBChatRepository implements ChatRepository {
}
}
sql.append(" ORDER BY Transactions.created_when");
sql.append(" ORDER BY created_when");
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
@@ -129,143 +128,109 @@ public class HSQLDBChatRepository implements ChatRepository {
return chatMessages;
} catch (SQLException e) {
throw new DataException("Unable to fetch matching chat transactions from repository", e);
}
}
@Override
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException {
String sql = "SELECT SenderNames.name, RecipientNames.name "
+ "FROM ChatTransactions "
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
+ "LEFT OUTER JOIN Names AS RecipientNames ON RecipientNames.owner = recipient "
+ "WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, chatTransactionData.getSignature())) {
if (resultSet == null)
return null;
String senderName = resultSet.getString(1);
String recipientName = resultSet.getString(2);
long timestamp = chatTransactionData.getTimestamp();
int groupId = chatTransactionData.getTxGroupId();
byte[] reference = chatTransactionData.getReference();
byte[] senderPublicKey = chatTransactionData.getSenderPublicKey();
String sender = chatTransactionData.getSender();
String recipient = chatTransactionData.getRecipient();
byte[] chatReference = chatTransactionData.getChatReference();
byte[] data = chatTransactionData.getData();
boolean isText = chatTransactionData.getIsText();
boolean isEncrypted = chatTransactionData.getIsEncrypted();
byte[] signature = chatTransactionData.getSignature();
return new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch convert chat transaction from repository", e);
throw new DataException("Unable to fetch matching chat messages from repository", e);
}
}
@Override
public ActiveChats getActiveChats(String address) throws DataException {
List<GroupChat> groupChats = getActiveGroupChats(address);
List<GroupChat> groupChats = new ArrayList<>(); // TODO: getActiveGroupChats(address);
List<DirectChat> directChats = getActiveDirectChats(address);
return new ActiveChats(groupChats, directChats);
}
private List<GroupChat> getActiveGroupChats(String address) throws DataException {
// Find groups where address is a member and potential latest message details
String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name "
+ "FROM GroupMembers "
+ "JOIN Groups USING (group_id) "
+ "LEFT OUTER JOIN LATERAL("
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name "
+ "FROM ChatTransactions "
+ "JOIN Transactions USING (signature) "
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
// NOTE: We need to qualify "Groups.group_id" here to avoid "General error" bug in HSQLDB v2.5.0
+ "WHERE tx_group_id = Groups.group_id AND type = " + TransactionType.CHAT.value + " "
+ "ORDER BY created_when DESC "
+ "LIMIT 1"
+ ") AS LatestMessages ON TRUE "
+ "WHERE address = ?";
List<GroupChat> groupChats = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(groupsSql, address)) {
if (resultSet != null) {
do {
int groupId = resultSet.getInt(1);
String groupName = resultSet.getString(2);
Long timestamp = resultSet.getLong(3);
if (timestamp == 0 && resultSet.wasNull())
timestamp = null;
String sender = resultSet.getString(4);
String senderName = resultSet.getString(5);
GroupChat groupChat = new GroupChat(groupId, groupName, timestamp, sender, senderName);
groupChats.add(groupChat);
} while (resultSet.next());
}
} catch (SQLException e) {
throw new DataException("Unable to fetch active group chats from repository", e);
}
// We need different SQL to handle group-less chat
String grouplessSql = "SELECT created_when, sender, SenderNames.name "
+ "FROM ChatTransactions "
+ "JOIN Transactions USING (signature) "
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
+ "WHERE tx_group_id = 0 "
+ "AND recipient IS NULL "
+ "ORDER BY created_when DESC "
+ "LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(grouplessSql)) {
Long timestamp = null;
String sender = null;
String senderName = null;
if (resultSet != null) {
// We found a recipient-less, group-less CHAT message, so report its details
timestamp = resultSet.getLong(1);
sender = resultSet.getString(2);
senderName = resultSet.getString(3);
}
GroupChat groupChat = new GroupChat(0, null, timestamp, sender, senderName);
groupChats.add(groupChat);
} catch (SQLException e) {
throw new DataException("Unable to fetch active group chats from repository", e);
}
return groupChats;
}
// private List<GroupChat> getActiveGroupChats(String address) throws DataException {
// // TODO: needs completely rethinking
// // Find groups where address is a member and potential latest message details
// String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name "
// + "FROM GroupMembers "
// + "JOIN Groups USING (group_id) "
// + "LEFT OUTER JOIN LATERAL("
// + "SELECT created_when AS latest_timestamp, sender, name AS sender_name "
// + "FROM ChatTransactions "
// + "JOIN Transactions USING (signature) "
// + "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
// // NOTE: We need to qualify "Groups.group_id" here to avoid "General error" bug in HSQLDB v2.5.0
// + "WHERE tx_group_id = Groups.group_id AND type = " + TransactionType.CHAT.value + " "
// + "ORDER BY created_when DESC "
// + "LIMIT 1"
// + ") AS LatestMessages ON TRUE "
// + "WHERE address = ?";
//
// List<GroupChat> groupChats = new ArrayList<>();
// try (ResultSet resultSet = this.repository.checkedExecute(groupsSql, address)) {
// if (resultSet != null) {
// do {
// int groupId = resultSet.getInt(1);
// String groupName = resultSet.getString(2);
//
// Long timestamp = resultSet.getLong(3);
// if (timestamp == 0 && resultSet.wasNull())
// timestamp = null;
//
// String sender = resultSet.getString(4);
// String senderName = resultSet.getString(5);
//
// GroupChat groupChat = new GroupChat(groupId, groupName, timestamp, sender, senderName);
// groupChats.add(groupChat);
// } while (resultSet.next());
// }
// } catch (SQLException e) {
// throw new DataException("Unable to fetch active group chats from repository", e);
// }
//
// // We need different SQL to handle group-less chat
// String grouplessSql = "SELECT created_when, sender, SenderNames.name "
// + "FROM ChatTransactions "
// + "JOIN Transactions USING (signature) "
// + "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
// + "WHERE tx_group_id = 0 "
// + "AND recipient IS NULL "
// + "ORDER BY created_when DESC "
// + "LIMIT 1";
//
// try (ResultSet resultSet = this.repository.checkedExecute(grouplessSql)) {
// Long timestamp = null;
// String sender = null;
// String senderName = null;
//
// if (resultSet != null) {
// // We found a recipient-less, group-less CHAT message, so report its details
// timestamp = resultSet.getLong(1);
// sender = resultSet.getString(2);
// senderName = resultSet.getString(3);
// }
//
// GroupChat groupChat = new GroupChat(0, null, timestamp, sender, senderName);
// groupChats.add(groupChat);
// } catch (SQLException e) {
// throw new DataException("Unable to fetch active group chats from repository", e);
// }
//
// return groupChats;
// }
private List<DirectChat> getActiveDirectChats(String address) throws DataException {
// Find chat messages involving address
String directSql = "SELECT other_address, name, latest_timestamp, sender, sender_name "
+ "FROM ("
+ "SELECT recipient FROM ChatTransactions "
+ "SELECT recipient FROM ChatMessages "
+ "WHERE sender = ? AND recipient IS NOT NULL "
+ "UNION "
+ "SELECT sender FROM ChatTransactions "
+ "SELECT sender FROM ChatMessages "
+ "WHERE recipient = ?"
+ ") AS OtherParties (other_address) "
+ "CROSS JOIN LATERAL("
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name "
+ "FROM ChatTransactions "
+ "NATURAL JOIN Transactions "
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
+ "FROM ChatMessages "
// TODO: + "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
+ "WHERE (sender = other_address AND recipient = ?) "
+ "OR (sender = ? AND recipient = other_address) "
+ "ORDER BY created_when DESC "
+ "LIMIT 1"
+ ") AS LatestMessages "
+ "LEFT OUTER JOIN Names ON owner = other_address";
+ ") AS LatestMessages ";
// TODO: + "LEFT OUTER JOIN Names ON owner = other_address";
Object[] bindParams = new Object[] { address, address, address, address };
@@ -279,7 +244,7 @@ public class HSQLDBChatRepository implements ChatRepository {
String name = resultSet.getString(2);
long timestamp = resultSet.getLong(3);
String sender = resultSet.getString(4);
String senderName = resultSet.getString(5);
String senderName = "TODO: sender name "; //resultSet.getString(5); // TODO: fetch separately?
DirectChat directChat = new DirectChat(otherAddress, name, timestamp, sender, senderName);
directChats.add(directChat);
@@ -291,4 +256,59 @@ public class HSQLDBChatRepository implements ChatRepository {
return directChats;
}
public ChatMessage getChatMessageBySignature(byte[] signature) throws DataException {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT created_when, tx_group_id, reference, creator, sender, recipient, "
// TODO: + "SenderNames.name, RecipientNames.name, "
+ "chat_reference, data, is_text, is_encrypted, signature "
+ "FROM ChatMessages WHERE signature = ?");
// TODO: + "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
// TODO: + "LEFT OUTER JOIN Names AS RecipientNames ON RecipientNames.owner = recipient ");
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), signature)) {
if (resultSet == null)
return null;
long timestamp = resultSet.getLong(1);
int groupId = resultSet.getInt(2);
byte[] reference = resultSet.getBytes(3);
byte[] senderPublicKey = resultSet.getBytes(4);
String sender = resultSet.getString(5);
String senderName = resultSet.getString(6); // TOOD
String recipient = resultSet.getString(7);
String recipientName = resultSet.getString(8); // TODO
byte[] chatReference = resultSet.getBytes(9);
byte[] data = resultSet.getBytes(10);
boolean isText = resultSet.getBoolean(11);
boolean isEncrypted = resultSet.getBoolean(12);
byte[] signatureResult = resultSet.getBytes(13);
ChatMessage chatMessage = new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signatureResult);
return chatMessage;
} catch (SQLException e) {
throw new DataException("Unable to fetch chat message from repository", e);
}
}
@Override
public void save(ChatMessage chatMessage) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("ChatMessages");
saveHelper.bind("timestamp", chatMessage.getTimestamp()).bind("tx_group_id", chatMessage.getTxGroupId())
.bind("reference", chatMessage.getReference()).bind("sender_public_key", chatMessage.getSenderPublicKey())
.bind("sender", chatMessage.getSignature()).bind("recipient", chatMessage.getRecipient())
.bind("data", chatMessage.getData()).bind("is_text", chatMessage.isText())
.bind("is_encrypted", chatMessage.isEncrypted()).bind("signature", chatMessage.getSignature());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save chat message into repository", e);
}
}
}

View File

@@ -99,7 +99,7 @@ public class HSQLDBDatabasePruning {
// It's essential that we rebuild the latest AT states here, as we are using this data in the next query.
// Failing to do this will result in important AT states being deleted, rendering the database unusable.
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(endHeight);
// Loop through all the LatestATStates and copy them to the new table

View File

@@ -58,7 +58,7 @@ public class HSQLDBDatabaseUpdates {
* @return database version, or 0 if no schema yet
* @throws SQLException
*/
private static int fetchDatabaseVersion(Connection connection) throws SQLException {
protected static int fetchDatabaseVersion(Connection connection) throws SQLException {
try (Statement stmt = connection.createStatement()) {
if (stmt.execute("SELECT version FROM DatabaseInfo"))
try (ResultSet resultSet = stmt.getResultSet()) {
@@ -988,6 +988,11 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE INDEX ChatTransactionsChatReferenceIndex ON ChatTransactions (chat_reference)");
break;
case 46:
// We need to track the sale price when canceling a name sale, so it can be put back when orphaned
stmt.execute("ALTER TABLE CancelSellNameTransactions ADD sale_price QortalAmount");
break;
default:
// nothing to do
return false;

View File

@@ -28,7 +28,6 @@ public class HSQLDBMessageRepository implements MessageRepository {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT signature from MessageTransactions "
+ "JOIN Transactions USING (signature) "
+ "JOIN BlockTransactions ON transaction_signature = signature "
+ "WHERE ");
List<String> whereClauses = new ArrayList<>();

View File

@@ -18,6 +18,11 @@ import org.qortal.settings.Settings;
public class HSQLDBRepositoryFactory implements RepositoryFactory {
public enum HSQLDBRepositoryType {
MAIN,
CHAT
}
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepositoryFactory.class);
/** Log getConnection() calls that take longer than this. (ms) */
@@ -25,18 +30,21 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
private String connectionUrl;
private HSQLDBPool connectionPool;
private HSQLDBRepositoryType type;
private final boolean wasPristine;
/**
* Constructs new RepositoryFactory using passed <tt>connectionUrl</tt>.
* Constructs new RepositoryFactory using passed <tt>connectionUrl</tt> and <tt>type</tt>.
*
* @param connectionUrl
* @param type
* @throws DataException <i>without throwable</i> if repository in use by another process.
* @throws DataException <i>with throwable</i> if repository cannot be opened for some other reason.
*/
public HSQLDBRepositoryFactory(String connectionUrl) throws DataException {
public HSQLDBRepositoryFactory(String connectionUrl, HSQLDBRepositoryType type) throws DataException {
// one-time initialization goes in here
this.connectionUrl = connectionUrl;
this.type = type;
// Check no-one else is accessing database
try (Connection connection = DriverManager.getConnection(this.connectionUrl)) {
@@ -66,12 +74,36 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
// Perform DB updates?
try (final Connection connection = this.connectionPool.getConnection()) {
this.wasPristine = HSQLDBDatabaseUpdates.updateDatabase(connection);
switch (this.type) {
case MAIN:
this.wasPristine = HSQLDBDatabaseUpdates.updateDatabase(connection);
break;
case CHAT:
this.wasPristine = HSQLDBChatDatabaseUpdates.updateDatabase(connection);
break;
default:
this.wasPristine = false;
throw new DataException(String.format("No updates defined for %s repository", this.type));
}
} catch (SQLException e) {
throw new DataException("Repository initialization error", e);
}
}
/**
* Constructs new RepositoryFactory using passed <tt>connectionUrl</tt>, using the <tt>MAIN</tt> repository type.
*
* @param connectionUrl
* @throws DataException <i>without throwable</i> if repository in use by another process.
* @throws DataException <i>with throwable</i> if repository cannot be opened for some other reason.
*/
public HSQLDBRepositoryFactory(String connectionUrl) throws DataException {
this(connectionUrl, HSQLDBRepositoryType.MAIN);
}
@Override
public boolean wasPristineAtOpen() {
return this.wasPristine;
@@ -79,7 +111,7 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
@Override
public RepositoryFactory reopen() throws DataException {
return new HSQLDBRepositoryFactory(this.connectionUrl);
return new HSQLDBRepositoryFactory(this.connectionUrl, this.type);
}
@Override

View File

@@ -17,15 +17,16 @@ public class HSQLDBCancelSellNameTransactionRepository extends HSQLDBTransaction
}
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT name FROM CancelSellNameTransactions WHERE signature = ?";
String sql = "SELECT name, sale_price FROM CancelSellNameTransactions WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
if (resultSet == null)
return null;
String name = resultSet.getString(1);
Long salePrice = resultSet.getLong(2);
return new CancelSellNameTransactionData(baseTransactionData, name);
return new CancelSellNameTransactionData(baseTransactionData, name, salePrice);
} catch (SQLException e) {
throw new DataException("Unable to fetch cancel sell name transaction from repository", e);
}
@@ -38,7 +39,7 @@ public class HSQLDBCancelSellNameTransactionRepository extends HSQLDBTransaction
HSQLDBSaver saveHelper = new HSQLDBSaver("CancelSellNameTransactions");
saveHelper.bind("signature", cancelSellNameTransactionData.getSignature()).bind("owner", cancelSellNameTransactionData.getOwnerPublicKey()).bind("name",
cancelSellNameTransactionData.getName());
cancelSellNameTransactionData.getName()).bind("sale_price", cancelSellNameTransactionData.getSalePrice());
try {
saveHelper.execute(this.repository);

View File

@@ -259,6 +259,8 @@ public class Settings {
private Long slowQueryThreshold = null;
/** Repository storage path. */
private String repositoryPath = "db";
/** Chat repository storage path. */
private String chatRepositoryPath = "chatdb";
/** Repository connection pool size. Needs to be a bit bigger than maxNetworkThreadPoolSize */
private int repositoryConnectionPoolSize = 100;
private List<String> fixedNetwork;
@@ -778,6 +780,10 @@ public class Settings {
return this.repositoryPath;
}
public String getChatRepositoryPath() {
return this.chatRepositoryPath;
}
public int getRepositoryConnectionPoolSize() {
return this.repositoryConnectionPoolSize;
}

View File

@@ -12,6 +12,8 @@ public abstract class Transformer {
// Raw, not Base58-encoded
public static final int ADDRESS_LENGTH = 25;
// Base58-encoded
public static final int BASE58_ADDRESS_LENGTH = 36;
public static final int PUBLIC_KEY_LENGTH = 32;
public static final int PRIVATE_KEY_LENGTH = 32;

View File

@@ -0,0 +1,83 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# Keys are from api.ApiError enum
# "localeLang": "pl",
### Common ###
JSON = nie udało się przetworzyć wiadomości JSON
INSUFFICIENT_BALANCE = niedostateczne środki
UNAUTHORIZED = nieautoryzowane połączenie API
REPOSITORY_ISSUE = błąd repozytorium
NON_PRODUCTION = to wywołanie API nie jest dozwolone dla systemów produkcyjnych
BLOCKCHAIN_NEEDS_SYNC = blockchain musi się najpierw zsynchronizować
NO_TIME_SYNC = zegar się jeszcze nie zsynchronizował
### Validation ###
INVALID_SIGNATURE = nieprawidłowa sygnatura
INVALID_ADDRESS = nieprawidłowy adres
INVALID_PUBLIC_KEY = nieprawidłowy klucz publiczny
INVALID_DATA = nieprawidłowe dane
INVALID_NETWORK_ADDRESS = nieprawidłowy adres sieci
ADDRESS_UNKNOWN = nieznany adres konta
INVALID_CRITERIA = nieprawidłowe kryteria wyszukiwania
INVALID_REFERENCE = nieprawidłowe skierowanie
TRANSFORMATION_ERROR = nie udało się przekształcić JSON w transakcję
INVALID_PRIVATE_KEY = klucz prywatny jest niepoprawny
INVALID_HEIGHT = nieprawidłowa wysokość bloku
CANNOT_MINT = konto nie możne bić monet
### Blocks ###
BLOCK_UNKNOWN = blok nieznany
### Transactions ###
TRANSACTION_UNKNOWN = nieznana transakcja
PUBLIC_KEY_NOT_FOUND = nie znaleziono klucza publicznego
# this one is special in that caller expected to pass two additional strings, hence the two %s
TRANSACTION_INVALID = transakcja nieważna: %s (%s)
### Naming ###
NAME_UNKNOWN = nazwa nieznana
### Asset ###
INVALID_ASSET_ID = nieprawidłowy identyfikator aktywy
INVALID_ORDER_ID = nieprawidłowy identyfikator zlecenia aktywy
ORDER_UNKNOWN = nieznany identyfikator zlecenia aktywy
### Groups ###
GROUP_UNKNOWN = nieznana grupa
### Foreign Blockchain ###
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = obcy blockchain lub problem z siecią ElectrumX
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = niewystarczające środki na obcym blockchainie
FOREIGN_BLOCKCHAIN_TOO_SOON = zbyt wczesne nadawanie transakcji na obcym blockchainie (okres karencji/średni czas bloku)
### Trade Portal ###
ORDER_SIZE_TOO_SMALL = zbyt niska kwota zlecenia
### Data ###
FILE_NOT_FOUND = plik nie został znaleziony
NO_REPLY = peer nie odpowiedział w wyznaczonym czasie

View File

@@ -16,7 +16,7 @@ NON_PRODUCTION = этот вызов API не разрешен для произ
BLOCKCHAIN_NEEDS_SYNC = блокчейн должен сначала синхронизироваться
NO_TIME_SYNC = пока нет синхронизации часов
NO_TIME_SYNC = время не синхронизировано
### Validation ###
INVALID_SIGNATURE = недействительная подпись
@@ -72,7 +72,7 @@ FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = проблема с внешним блокч
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = недостаточный баланс на внешнем блокчейне
FOREIGN_BLOCKCHAIN_TOO_SOON = слишком рано для трансляции транзакции во внений блокчей (время блокировки/среднее время блока)
FOREIGN_BLOCKCHAIN_TOO_SOON = слишком рано для трансляции транзакции во внешний блокчей (время блокировки/среднее время блока)
### Trade Portal ###
ORDER_SIZE_TOO_SMALL = слишком маленькая сумма ордера
@@ -80,4 +80,4 @@ ORDER_SIZE_TOO_SMALL = слишком маленькая сумма ордера
### Data ###
FILE_NOT_FOUND = файл не найден
NO_REPLY = узел не ответил данными
NO_REPLY = нет ответа

View File

@@ -5,11 +5,11 @@ APPLYING_UPDATE_AND_RESTARTING = Automatisches Update anwenden und neu starten
AUTO_UPDATE = Automatisches Update
BLOCK_HEIGHT = height
BLOCK_HEIGHT = Blockhöhe
BLOCKS_REMAINING = blocks remaining
BUILD_VERSION = Build-Version
BUILD_VERSION = Entwicklungs-Version
CHECK_TIME_ACCURACY = Prüfe Zeitgenauigkeit
@@ -23,7 +23,7 @@ CREATING_BACKUP_OF_DB_FILES = Erstellen Backup von Datenbank Dateien …
DB_BACKUP = Datenbank Backup
DB_CHECKPOINT = Datenbank Kontrollpunkt
DB_CHECKPOINT = Datenbank Check
DB_MAINTENANCE = Datenbank Instandhaltung
@@ -31,18 +31,18 @@ EXIT = Verlassen
LITE_NODE = Lite node
MINTING_DISABLED = NOT minting
MINTING_DISABLED = Kein minting
MINTING_ENABLED = \u2714 Minting
MINTING_ENABLED = \u2714 Minting aktiviert
OPEN_UI = Öffne UI
PERFORMING_DB_CHECKPOINT = Speichern nicht übergebener Datenbank Änderungen
PERFORMING_DB_CHECKPOINT = Speichern von unbestätigten Datenbankänderungen...
PERFORMING_DB_MAINTENANCE = Planmäßige Wartung durchführen...
SYNCHRONIZE_CLOCK = Synchronisiere Uhr
SYNCHRONIZING_BLOCKCHAIN = Synchronisierung
SYNCHRONIZING_BLOCKCHAIN = Synchronisierung der Blockchain
SYNCHRONIZING_CLOCK = Synchronisierung Uhr
SYNCHRONIZING_CLOCK = Synchronisierung der Uhr

View File

@@ -0,0 +1,46 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
APPLYING_UPDATE_AND_RESTARTING = Zastosowanie automatycznej aktualizacji i ponowne uruchomienie...
AUTO_UPDATE = Automatyczna aktualizacja
BLOCK_HEIGHT = wysokość
BUILD_VERSION = Wersja kompilacji
CHECK_TIME_ACCURACY = Sprawdz dokładność czasu
CONNECTING = Łączenie
CONNECTION = połączenie
CONNECTIONS = połączenia
CREATING_BACKUP_OF_DB_FILES = Tworzenie kopii zapasowej plików bazy danych...
DB_BACKUP = Kopia zapasowa bazy danych
DB_CHECKPOINT = Punkt kontrolny bazy danych...
DB_MAINTENANCE = Konserwacja bazy danych
EXIT = Zakończ
LITE_NODE = Lite node
MINTING_DISABLED = Mennica zamknięta
MINTING_ENABLED = \u2714 Mennica aktywna
OPEN_UI = Otwórz interfejs użytkownika
PERFORMING_DB_CHECKPOINT = Zapisywanie niezaksięgowanych zmian w bazie danych...
PERFORMING_DB_MAINTENANCE = Performing scheduled maintenance...
SYNCHRONIZE_CLOCK = Synchronizuj zegar
SYNCHRONIZING_BLOCKCHAIN = Synchronizacja
SYNCHRONIZING_CLOCK = Synchronizacja zegara

View File

@@ -1,7 +1,7 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
APPLYING_UPDATE_AND_RESTARTING = Применение автоматического обновления и перезапуска...
APPLYING_UPDATE_AND_RESTARTING = Применение автоматического обновления и перезапуск...
AUTO_UPDATE = Автоматическое обновление

View File

@@ -0,0 +1,195 @@
#
ACCOUNT_ALREADY_EXISTS = Account existiert bereits
ACCOUNT_CANNOT_REWARD_SHARE = Account kann keine Belohnung teilen
ADDRESS_ABOVE_RATE_LIMIT = address hat das angegebene Geschwindigkeitlimit erreicht
ADDRESS_BLOCKED = Addresse ist geblockt
ALREADY_GROUP_ADMIN = bereits Gruppen Admin
ALREADY_GROUP_MEMBER = bereits Gruppen Mitglied
ALREADY_VOTED_FOR_THAT_OPTION = bereits für diese Option gestimmt
ASSET_ALREADY_EXISTS = asset existiert bereits
ASSET_DOES_NOT_EXIST = asset nicht gefunden
ASSET_DOES_NOT_MATCH_AT = asset passt nicht mit AT's asset
ASSET_NOT_SPENDABLE = asset ist nicht ausgabefähig
AT_ALREADY_EXISTS = AT existiert bereits
AT_IS_FINISHED = AT ist fertig
AT_UNKNOWN = AT unbekannt
BAN_EXISTS = ban besteht bereits
BAN_UNKNOWN = ban unbekannt
BANNED_FROM_GROUP = von der gruppe gebannt
BUYER_ALREADY_OWNER = Käufer ist bereits Besitzer
CLOCK_NOT_SYNCED = Uhr nicht synchronisiert
DUPLICATE_MESSAGE = Adresse sendete doppelte Nachricht
DUPLICATE_OPTION = Duplizierungsmöglichkeit
GROUP_ALREADY_EXISTS = Gruppe besteht bereits
GROUP_APPROVAL_DECIDED = Gruppenfreigabe bereits beschlossen
GROUP_APPROVAL_NOT_REQUIRED = Gruppenfreigabe nicht erforderlich
GROUP_DOES_NOT_EXIST = Gruppe nicht vorhanden
GROUP_ID_MISMATCH = Gruppen-ID stimmt nicht überein
GROUP_OWNER_CANNOT_LEAVE = Gruppenbesitzer kann Gruppe nicht verlassen
HAVE_EQUALS_WANT = das bessesene-asset ist das selbe wie das gesuchte-asset
INCORRECT_NONCE = falsche PoW-Nonce
INSUFFICIENT_FEE = unzureichende Gebühr
INVALID_ADDRESS = ungültige Adresse
INVALID_AMOUNT = ungültiger Betrag
INVALID_ASSET_OWNER = Ungültiger Eigentümer
INVALID_AT_TRANSACTION = ungültige AT-Transaktion
INVALID_AT_TYPE_LENGTH = ungültige AT 'Typ' Länge
INVALID_BUT_OK = ungültig aber OK
INVALID_CREATION_BYTES = ungültige Erstellungs der bytes
INVALID_DATA_LENGTH = ungültige Datenlänge
INVALID_DESCRIPTION_LENGTH = ungültige Länge der Beschreibung
INVALID_GROUP_APPROVAL_THRESHOLD = ungültiger Schwellenwert für die Gruppenzulassung
INVALID_GROUP_BLOCK_DELAY = Ungültige Blockverzögerung der Gruppenfreigabe
INVALID_GROUP_ID = ungültige Gruppen-ID
INVALID_GROUP_OWNER = ungültiger Gruppenbesitzer
INVALID_LIFETIME = unzulässige Lebensdauer
INVALID_NAME_LENGTH = ungültige Namenslänge
INVALID_NAME_OWNER = ungültiger Besitzername
INVALID_OPTION_LENGTH = ungültige Länge der Optionen
INVALID_OPTIONS_COUNT = Anzahl ungültiger Optionen
INVALID_ORDER_CREATOR = ungültiger Auftragsersteller
INVALID_PAYMENTS_COUNT = Anzahl ungültiger Zahlungen
INVALID_PUBLIC_KEY = ungültiger öffentlicher Schlüssel
INVALID_QUANTITY = unzulässige Menge
INVALID_REFERENCE = ungültige Referenz
INVALID_RETURN = ungültige Rückgabe
INVALID_REWARD_SHARE_PERCENT = ungültig Prozent der Belohnunganteile
INVALID_SELLER = unzulässiger Verkäufer
INVALID_TAGS_LENGTH = ungültige 'tags'-Länge
INVALID_TIMESTAMP_SIGNATURE = Ungültige Zeitstempel-Signatur
INVALID_TX_GROUP_ID = Ungültige Transaktionsgruppen-ID
INVALID_VALUE_LENGTH = ungültige 'Wert'-Länge
INVITE_UNKNOWN = Gruppeneinladung unbekannt
JOIN_REQUEST_EXISTS = Gruppeneinladung existiert bereits
MAXIMUM_REWARD_SHARES = die maximale Anzahl von Reward-Shares für dieses Konto erreicht
MISSING_CREATOR = fehlender Ersteller
MULTIPLE_NAMES_FORBIDDEN = mehrere registrierte Namen pro Konto sind untersagt
NAME_ALREADY_FOR_SALE = Name bereits zum Verkauf
NAME_ALREADY_REGISTERED = Name bereits registriert
NAME_BLOCKED = Name geblockt
NAME_DOES_NOT_EXIST = Name nicht vorhanden
NAME_NOT_FOR_SALE = Name ist unverkäuflich
NAME_NOT_NORMALIZED = Name nicht in Unicode-'normalisierter' Form
NEGATIVE_AMOUNT = ungültiger/negativer Betrag
NEGATIVE_FEE = ungültige/negative Gebühr
NEGATIVE_PRICE = ungültiger/negativer Preis
NO_BALANCE = unzureichendes Guthaben
NO_BLOCKCHAIN_LOCK = die Blockchain des Knotens ist beschäftigt
NO_FLAG_PERMISSION = Konto hat diese Berechtigung nicht
NOT_GROUP_ADMIN = Account ist kein Gruppenadmin
NOT_GROUP_MEMBER = Account kein Gruppenmitglied
NOT_MINTING_ACCOUNT = Account kann nicht minten
NOT_YET_RELEASED = Funktion noch nicht freigegeben
OK = OK
ORDER_ALREADY_CLOSED = Asset Trade Order ist bereits geschlossen
ORDER_DOES_NOT_EXIST = asset trade order existiert nicht
POLL_ALREADY_EXISTS = Umfrage bereits vorhanden
POLL_DOES_NOT_EXIST = Umfrage nicht vorhanden
POLL_OPTION_DOES_NOT_EXIST = Umfrageoption existiert nicht
PUBLIC_KEY_UNKNOWN = öffentlicher Schlüssel unbekannt
REWARD_SHARE_UNKNOWN = Geteilte Belohnungen unbekant
SELF_SHARE_EXISTS = Selbstbeteiligung (Geteilte Belohnungen) sind breits vorhanden
TIMESTAMP_TOO_NEW = Zeitstempel zu neu
TIMESTAMP_TOO_OLD = Zeitstempel zu alt
TOO_MANY_UNCONFIRMED = Account hat zu viele unbestätigte Transaktionen am laufen
TRANSACTION_ALREADY_CONFIRMED = Transaktionen sind bereits bestätigt
TRANSACTION_ALREADY_EXISTS = Transaktionen existiert bereits
TRANSACTION_UNKNOWN = Unbekante Transaktion
TX_GROUP_ID_MISMATCH = Transaktion Gruppen ID stimmt nicht überein

View File

@@ -0,0 +1,196 @@
#
ACCOUNT_ALREADY_EXISTS = konto już istnieje
ACCOUNT_CANNOT_REWARD_SHARE = konto nie może udostępniać nagród
ADDRESS_ABOVE_RATE_LIMIT = adres osiągnął określony limit stawki
ADDRESS_BLOCKED = ten adres jest zablokowany
ALREADY_GROUP_ADMIN = już adminem grupy
ALREADY_GROUP_MEMBER = już członkiem grupy
ALREADY_VOTED_FOR_THAT_OPTION = już zagłosowano na ta opcje
ASSET_ALREADY_EXISTS = aktywa już istnieje
ASSET_DOES_NOT_EXIST = aktywa nie istnieje
ASSET_DOES_NOT_MATCH_AT = aktywa nie pasuje do aktywy AT
ASSET_NOT_SPENDABLE = aktywa nie jest rozporządzalna
AT_ALREADY_EXISTS = AT już istnieje
AT_IS_FINISHED = AT zakończył
AT_UNKNOWN = AT nieznany
BAN_EXISTS = ban już istnieje
BAN_UNKNOWN = ban nieznany
BANNED_FROM_GROUP = zbanowany z grupy
BUYER_ALREADY_OWNER = kupca jest już właścicielem
CLOCK_NOT_SYNCED = zegar nie zsynchronizowany
DUPLICATE_MESSAGE = adres wysłał duplikat wiadomości
DUPLICATE_OPTION = duplikat opcji
GROUP_ALREADY_EXISTS = grupa już istnieje
GROUP_APPROVAL_DECIDED = zatwierdzenie grupy już zdecydowano
GROUP_APPROVAL_NOT_REQUIRED = zatwierdzenie grupy nie jest wymagane
GROUP_DOES_NOT_EXIST = grupa nie istnieje
GROUP_ID_MISMATCH = niedopasowanie identyfikatora grupy
GROUP_OWNER_CANNOT_LEAVE = właściciel grupy nie może opuścić grupy
HAVE_EQUALS_WANT = posiadana aktywa równa się chcianej aktywie
INCORRECT_NONCE = nieprawidłowy nonce PoW
INSUFFICIENT_FEE = niewystarczająca opłata
INVALID_ADDRESS = nieprawidłowy adres
INVALID_AMOUNT = nieprawidłowa kwota
INVALID_ASSET_OWNER = nieprawidłowy właściciel aktywów
INVALID_AT_TRANSACTION = nieważna transakcja AT
INVALID_AT_TYPE_LENGTH = nieprawidłowa długość typu AT
INVALID_BUT_OK = nieważne, ale OK
INVALID_CREATION_BYTES = nieprawidłowe bajty tworzenia
INVALID_DATA_LENGTH = nieprawidłowa długość danych
INVALID_DESCRIPTION_LENGTH = nieprawidłowa długość opisu
INVALID_GROUP_APPROVAL_THRESHOLD = nieprawidłowy próg zatwierdzenia grupy
INVALID_GROUP_BLOCK_DELAY = nieprawidłowe opóźnienie bloku zatwierdzenia grupy
INVALID_GROUP_ID = nieprawidłowy identyfikator grupy
INVALID_GROUP_OWNER = nieprawidłowy właściciel grupy
INVALID_LIFETIME = nieprawidłowy czas istnienia
INVALID_NAME_LENGTH = nieprawidłowa długość nazwy
INVALID_NAME_OWNER = nieprawidłowy właściciel nazwy
INVALID_OPTION_LENGTH = nieprawidłowa długość opcji
INVALID_OPTIONS_COUNT = nieprawidłowa liczba opcji
INVALID_ORDER_CREATOR = nieprawidłowy twórca zlecenia
INVALID_PAYMENTS_COUNT = nieprawidłowa liczba płatności
INVALID_PUBLIC_KEY = nieprawidłowy klucz publiczny
INVALID_QUANTITY = nieprawidłowa ilość
INVALID_REFERENCE = nieprawidłowe skierowanie
INVALID_RETURN = nieprawidłowy zwrot
INVALID_REWARD_SHARE_PERCENT = nieprawidłowy procent udziału w nagrodzie
INVALID_SELLER = nieprawidłowy sprzedawca
INVALID_TAGS_LENGTH = nieprawidłowa długość tagów
INVALID_TIMESTAMP_SIGNATURE = nieprawidłowa sygnatura znacznika czasu
INVALID_TX_GROUP_ID = nieprawidłowy identyfikator grupy transakcji
INVALID_VALUE_LENGTH = nieprawidłowa długość wartości
INVITE_UNKNOWN = zaproszenie do grupy nieznane
JOIN_REQUEST_EXISTS = wniosek o dołączenie do grupy już istnieje
MAXIMUM_REWARD_SHARES = osiągnięto już maksymalną liczbę udziałów w nagrodzie dla tego konta
MISSING_CREATOR = brak twórcy
MULTIPLE_NAMES_FORBIDDEN = zabronione jest używanie wielu nazw na jednym koncie
NAME_ALREADY_FOR_SALE = nazwa już wystawiona na sprzedaż
NAME_ALREADY_REGISTERED = nazwa już zarejestrowana
NAME_BLOCKED = ta nazwa jest zablokowana
NAME_DOES_NOT_EXIST = nazwa nie istnieje
NAME_NOT_FOR_SALE = nazwa nie jest przeznaczona do sprzedaży
NAME_NOT_NORMALIZED = nazwa nie jest w formie 'znormalizowanej' Unicode
NEGATIVE_AMOUNT = nieprawidłowa/ujemna kwota
NEGATIVE_FEE = nieprawidłowa/ujemna opłata
NEGATIVE_PRICE = nieprawidłowa/ujemna cena
NO_BALANCE = niewystarczające środki
NO_BLOCKCHAIN_LOCK = węzeł blockchain jest obecnie zajęty
NO_FLAG_PERMISSION = konto nie ma tego uprawnienia
NOT_GROUP_ADMIN = konto nie jest adminem grupy
NOT_GROUP_MEMBER = konto nie jest członkiem grupy
NOT_MINTING_ACCOUNT = konto nie może bić monet
NOT_YET_RELEASED = funkcja nie została jeszcze udostępniona
OK = OK
ORDER_ALREADY_CLOSED = zlecenie handlu aktywami jest już zakończone
ORDER_DOES_NOT_EXIST = zlecenie sprzedaży aktywów nie istnieje
POLL_ALREADY_EXISTS = ankieta już istnieje
POLL_DOES_NOT_EXIST = ankieta nie istnieje
POLL_OPTION_DOES_NOT_EXIST = opcja ankiety nie istnieje
PUBLIC_KEY_UNKNOWN = klucz publiczny nieznany
REWARD_SHARE_UNKNOWN = nieznany udział w nagrodzie
SELF_SHARE_EXISTS = samoudział (udział w nagrodzie) już istnieje
TIMESTAMP_TOO_NEW = zbyt nowy znacznik czasu
TIMESTAMP_TOO_OLD = zbyt stary znacznik czasu
TOO_MANY_UNCONFIRMED = rachunek ma zbyt wiele niepotwierdzonych transakcji w toku
TRANSACTION_ALREADY_CONFIRMED = transakcja została już potwierdzona
TRANSACTION_ALREADY_EXISTS = transakcja już istnieje
TRANSACTION_UNKNOWN = transakcja nieznana
TX_GROUP_ID_MISMATCH = niezgodność ID grupy transakcji

View File

@@ -23,7 +23,6 @@ import org.qortal.transform.TransformationException;
import org.qortal.transform.block.BlockTransformation;
import org.qortal.utils.BlockArchiveUtils;
import org.qortal.utils.NTP;
import org.qortal.utils.Triple;
import java.io.File;
import java.io.IOException;
@@ -314,9 +313,10 @@ public class BlockArchiveTests extends Common {
repository.getBlockRepository().setBlockPruneHeight(901);
// Prune the AT states for the archived blocks
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(900);
repository.saveChanges();
int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900);
assertEquals(900-1, numATStatesPruned);
assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state
repository.getATRepository().setAtPruneHeight(901);
// Now ensure the SQL repository is missing blocks 2 and 900...
@@ -563,16 +563,23 @@ public class BlockArchiveTests extends Common {
// Trim the first 500 blocks
repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500);
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501);
repository.getATRepository().rebuildLatestAtStates(500);
repository.getATRepository().trimAtStates(0, 500, 1000);
repository.getATRepository().setAtTrimHeight(501);
// Now block 500 should only have the AT state data hash
block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500);
atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500);
// Now block 499 should only have the AT state data hash
List<ATStateData> block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499);
atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499);
assertNotNull(atStatesData.getStateHash());
assertNull(atStatesData.getStateData());
// ... but block 501 should have the full data
// ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range
block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500);
atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500);
assertNotNull(atStatesData.getStateHash());
assertNotNull(atStatesData.getStateData());
// ... and block 501 should also have the full data
List<ATStateData> block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501);
atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501);
assertNotNull(atStatesData.getStateHash());
@@ -612,9 +619,10 @@ public class BlockArchiveTests extends Common {
repository.getBlockRepository().setBlockPruneHeight(501);
// Prune the AT states for the archived blocks
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(500);
repository.saveChanges();
int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500);
assertEquals(499, numATStatesPruned);
assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state
repository.getATRepository().setAtPruneHeight(501);
// Now ensure the SQL repository is missing blocks 2 and 500...

View File

@@ -176,7 +176,8 @@ public class BootstrapTests extends Common {
repository.getBlockRepository().setBlockPruneHeight(901);
// Prune the AT states for the archived blocks
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(900);
repository.saveChanges();
repository.getATRepository().pruneAtStates(0, 900);
repository.getATRepository().setAtPruneHeight(901);

View File

@@ -0,0 +1,80 @@
package org.qortal.test;
import org.junit.Before;
import org.junit.Test;
import org.qortal.data.chat.ChatMessage;
import org.qortal.network.message.ChatMessagesMessage;
import org.qortal.network.message.MessageException;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.Common;
import org.qortal.test.common.TestAccount;
import org.qortal.utils.NTP;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import static org.junit.Assert.*;
public class ChatMessageTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testChatMessageSerialization() throws DataException, MessageException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
TestAccount bob = Common.getTestAccount(repository, "bob");
// Build chat message
long timestamp = NTP.getTime();
int txGroupId = 1;
byte[] chatReference = new byte[64];
new Random().nextBytes(chatReference);
byte[] messageData = new byte[80];
new Random().nextBytes(messageData);
byte[] signature = new byte[64];
new Random().nextBytes(signature);
ChatMessage aliceMessage = new ChatMessage(timestamp, txGroupId, alice.getLastReference(),
alice.getPublicKey(), alice.getAddress(), "alice", bob.getAddress(), "bob",
chatReference, messageData, true, true, signature);
// Serialize
ChatMessagesMessage chatMessagesMessage = new ChatMessagesMessage(Arrays.asList(aliceMessage));
byte[] serializedBytes = chatMessagesMessage.getDataBytes();
// Deserialize
ByteBuffer byteBuffer = ByteBuffer.wrap(serializedBytes);
ChatMessagesMessage deserializedChatMessagesMessage = (ChatMessagesMessage) ChatMessagesMessage.fromByteBuffer(0, byteBuffer);
List<ChatMessage> deserializedChatMessages = deserializedChatMessagesMessage.getChatMessages();
assertEquals(1, deserializedChatMessages.size());
ChatMessage deserializedChatMessage = deserializedChatMessages.get(0);
// Check all the values
assertEquals(timestamp, deserializedChatMessage.getTimestamp());
assertEquals(txGroupId, deserializedChatMessage.getTxGroupId());
assertArrayEquals(alice.getLastReference(), deserializedChatMessage.getReference());
assertArrayEquals(alice.getPublicKey(), deserializedChatMessage.getSenderPublicKey());
assertEquals(alice.getAddress(), deserializedChatMessage.getSender());
assertEquals("alice", deserializedChatMessage.getSenderName());
assertEquals(bob.getAddress(), deserializedChatMessage.getRecipient());
assertEquals("bob", deserializedChatMessage.getRecipientName());
assertArrayEquals(messageData, deserializedChatMessage.getData());
assertEquals(true, deserializedChatMessage.isText());
assertEquals(true, deserializedChatMessage.isEncrypted());
assertArrayEquals(signature, deserializedChatMessage.getSignature());
}
}
}

View File

@@ -1,16 +1,33 @@
package org.qortal.test;
import com.google.common.hash.HashCode;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.controller.BlockMinter;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.LitecoinACCTv3;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.AtUtils;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import java.util.ArrayList;
import java.util.List;
@@ -19,6 +36,13 @@ import static org.junit.Assert.*;
public class PruneTests extends Common {
// Constants for test AT (an LTC ACCT)
public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long litecoinAmount = 864200L; // 0.00864200 LTC
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@@ -62,23 +86,32 @@ public class PruneTests extends Common {
repository.getBlockRepository().setBlockPruneHeight(6);
// Prune AT states for blocks 2-5
repository.getATRepository().rebuildLatestAtStates(5);
repository.saveChanges();
int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 5);
assertEquals(4, numATStatesPruned);
assertEquals(3, numATStatesPruned);
repository.getATRepository().setAtPruneHeight(6);
// Make sure that blocks 2-5 are now missing block data and AT states data
for (Integer i=2; i <= 5; i++) {
// Make sure that blocks 2-4 are now missing block data and AT states data
for (Integer i=2; i <= 4; i++) {
BlockData blockData = repository.getBlockRepository().fromHeight(i);
assertNull(blockData);
List<ATStateData> atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(i);
assertTrue(atStatesDataList.isEmpty());
}
// ... but blocks 6-10 have block data and full AT states data
// Block 5 should have full AT states data even though it was pruned.
// This is because we identified that as the "latest" AT state in that block range
BlockData blockData = repository.getBlockRepository().fromHeight(5);
assertNull(blockData);
List<ATStateData> atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(5);
assertEquals(1, atStatesDataList.size());
// Blocks 6-10 have block data and full AT states data
for (Integer i=6; i <= 10; i++) {
BlockData blockData = repository.getBlockRepository().fromHeight(i);
blockData = repository.getBlockRepository().fromHeight(i);
assertNotNull(blockData.getSignature());
List<ATStateData> atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(i);
atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(i);
assertNotNull(atStatesDataList);
assertFalse(atStatesDataList.isEmpty());
ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(atStatesDataList.get(0).getATAddress(), i);
@@ -88,4 +121,102 @@ public class PruneTests extends Common {
}
}
@Test
public void testPruneSleepingAt() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = Common.getTestAccount(repository, "alice");
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Mint enough blocks to take the original DEPLOY_AT past the prune threshold (in this case 20)
Block block = BlockUtils.mintBlocks(repository, 25);
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = LitecoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
long txTimestamp = block.getBlockData().getTimestamp();
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress, txTimestamp);
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
// Prune AT states up to block 20
repository.getATRepository().rebuildLatestAtStates(20);
repository.saveChanges();
int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 20);
assertEquals(1, numATStatesPruned); // deleted state at heights 2, but state at height 3 remains
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Test orphaning - should be possible because the previous AT state at height 3 is still available
BlockUtils.orphanLastBlock(repository);
}
}
// Helper methods for AT testing
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = LitecoinACCTv3.buildQortalAT(tradeAddress, litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
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 = "QORT-LTC cross-chain trade";
String description = String.format("Qortal-Litecoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-LTC ACCT";
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;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient, long txTimestamp) throws DataException {
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
}

View File

@@ -2,29 +2,20 @@ 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.AtUtils;
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 {
@@ -76,7 +67,7 @@ public class AtRepositoryTests extends Common {
Integer testHeight = maxHeight - 2;
// Trim AT state data
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(maxHeight);
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
@@ -130,7 +121,7 @@ public class AtRepositoryTests extends Common {
Integer testHeight = blockchainHeight;
// Trim AT state data
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(maxHeight);
// COMMIT to check latest AT states persist / TEMPORARY table interaction
repository.saveChanges();
@@ -163,8 +154,8 @@ public class AtRepositoryTests extends Common {
int maxTrimHeight = blockchainHeight - 4;
Integer testHeight = maxTrimHeight + 1;
// Trim AT state data
repository.getATRepository().rebuildLatestAtStates();
// Trim AT state data (using a max height of maxTrimHeight + 1, so it is beyond the trimmed range)
repository.getATRepository().rebuildLatestAtStates(maxTrimHeight + 1);
repository.saveChanges();
repository.getATRepository().trimAtStates(2, maxTrimHeight, 1000);
@@ -333,7 +324,7 @@ public class AtRepositoryTests extends Common {
Integer testHeight = maxHeight - 2;
// Trim AT state data
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(maxHeight);
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);

View File

@@ -20,6 +20,15 @@ public class BlockUtils {
return BlockMinter.mintTestingBlock(repository, mintingAccount);
}
/** Mints multiple blocks using "alice-reward-share" test account, and returns the final block. */
public static Block mintBlocks(Repository repository, int count) throws DataException {
Block block = null;
for (int i=0; i<count; i++) {
block = BlockUtils.mintBlock(repository);
}
return block;
}
public static Long getNextBlockReward(Repository repository) throws DataException {
int currentHeight = repository.getBlockRepository().getBlockchainHeight();

View File

@@ -165,6 +165,52 @@ public class BuySellTests extends Common {
assertEquals("price incorrect", price, nameData.getSalePrice());
}
@Test
public void testCancelSellNameAndRelist() throws DataException {
// Register-name and sell-name
testSellName();
// Cancel Sell-name
CancelSellNameTransactionData transactionData = new CancelSellNameTransactionData(TestTransaction.generateBase(alice), name);
TransactionUtils.signAndMint(repository, transactionData, alice);
NameData nameData;
// Check name is no longer for sale
nameData = repository.getNameRepository().fromName(name);
assertFalse(nameData.isForSale());
assertNull(nameData.getSalePrice());
// Re-sell-name
Long newPrice = random.nextInt(1000) * Amounts.MULTIPLIER;
SellNameTransactionData sellNameTransactionData = new SellNameTransactionData(TestTransaction.generateBase(alice), name, newPrice);
TransactionUtils.signAndMint(repository, sellNameTransactionData, alice);
// Check name is for sale
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.isForSale());
assertEquals("price incorrect", newPrice, nameData.getSalePrice());
// Orphan sell-name
BlockUtils.orphanLastBlock(repository);
// Check name no longer for sale
nameData = repository.getNameRepository().fromName(name);
assertFalse(nameData.isForSale());
assertNull(nameData.getSalePrice());
// Orphan cancel-sell-name
BlockUtils.orphanLastBlock(repository);
// Check name is for sale (at original price)
nameData = repository.getNameRepository().fromName(name);
assertTrue(nameData.isForSale());
assertEquals("price incorrect", price, nameData.getSalePrice());
// Orphan sell-name and register-name
BlockUtils.orphanBlocks(repository, 2);
}
@Test
public void testBuyName() throws DataException {
// Register-name and sell-name