mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-30 05:31:23 +00:00
Compare commits
138 Commits
chat-refer
...
v3.8.7
Author | SHA1 | Date | |
---|---|---|---|
|
52c806f9e6 | ||
|
b2d31a7e02 | ||
|
cfa0b1d8ea | ||
|
edacce1bac | ||
|
074cba2266 | ||
|
f4a32d19dd | ||
|
eb6d84c04d | ||
|
26587067d8 | ||
|
227d93a31e | ||
|
830bae3dc1 | ||
|
ec09312cc5 | ||
|
11654ba9c6 | ||
|
ea356d1026 | ||
|
e7a3e511bd | ||
|
6fca30ce75 | ||
|
e903e59f7f | ||
|
bef170df7e | ||
|
386bfa4e20 | ||
|
6f867031e2 | ||
|
8f589391a6 | ||
|
30c9f63cb1 | ||
|
952b21d9bd | ||
|
1f410a503e | ||
|
f5e30eeaf5 | ||
|
21f5d9a3d0 | ||
|
ab34fae810 | ||
|
42f2d015b7 | ||
|
2181ece28d | ||
|
03a5d0e5f9 | ||
|
352f094272 | ||
|
c5c826453b | ||
|
e86b9b1caf | ||
|
7fc170575c | ||
|
876658256f | ||
|
a24ba40d5c | ||
|
06d8a21714 | ||
|
ae44065d7e | ||
|
6ad0989ea2 | ||
|
5962ebd08a | ||
|
bf06d47842 | ||
|
8c708558cb | ||
|
6b36d94c6f | ||
|
1d568fa462 | ||
|
328ba48224 | ||
|
6196841609 | ||
|
9f30571b12 | ||
|
1f7fec6251 | ||
|
c3f19ea0c1 | ||
|
e31515b4a2 | ||
|
8ad46b6344 | ||
|
2f7912abce | ||
|
64529e8abf | ||
|
9d81ea7744 | ||
|
688acd466c | ||
|
81cf46f5dd | ||
|
4c52d6f0fc | ||
|
de47a94677 | ||
|
bd4c47dba6 | ||
|
c03f271825 | ||
|
dfe3754afc | ||
|
30105199a2 | ||
|
e91e612b55 | ||
|
2a55eba1f7 | ||
|
39e59cbcf8 | ||
|
016191bdb0 | ||
|
0596a07c7d | ||
|
c62c59b445 | ||
|
f78101e9cc | ||
|
476fdcb31d | ||
|
02d5043ef7 | ||
|
0ad9e2f65b | ||
|
4dc0033a5a | ||
|
745cfe8ea1 | ||
|
6284a4691c | ||
|
41f88be55e | ||
|
ba95f8376f | ||
|
8e97c05b56 | ||
|
eb569304ba | ||
|
b0486f44bb | ||
|
cecf28ab7b | ||
|
98b92a5bf1 | ||
|
6b45901c47 | ||
|
166f9bd079 | ||
|
2f8f896077 | ||
|
9a77aff0a6 | ||
|
c6d65a88dc | ||
|
4aea29a91b | ||
|
0e81665a36 | ||
|
2a4ac1ed24 | ||
|
bb74b2d4f6 | ||
|
758a02d71a | ||
|
7ae142fa64 | ||
|
a75ed0e634 | ||
|
e40dc4af59 | ||
|
e678ea22e0 | ||
|
cf3195cb83 | ||
|
80048208d1 | ||
|
08de1fb4ec | ||
|
99d5bf9103 | ||
|
1dc7f056f9 | ||
|
cdeb2052b0 | ||
|
5c9109aca9 | ||
|
ccc1976d00 | ||
|
12fb6cd0ad | ||
|
6f95e7c1c8 | ||
|
a69618133e | ||
|
51ad0a5b48 | ||
|
45a6f495d2 | ||
|
4d9964c080 | ||
|
9afc31a20d | ||
|
d435e4047b | ||
|
c108afa27c | ||
|
eea42b56ee | ||
|
f4d20e42f3 | ||
|
f14cc374c6 | ||
|
99ba4caf75 | ||
|
ae991dda4d | ||
|
5ff7b3df6d | ||
|
76686eca21 | ||
|
3965f24ab5 | ||
|
a75fd14e45 | ||
|
41cdf665ed | ||
|
6ea3c0e6f7 | ||
|
5f0263c078 | ||
|
58e5d325ff | ||
|
7003a8274b | ||
|
ab687af4bb | ||
|
f50c0c87dd | ||
|
9c3a4d6e37 | ||
|
1c8a6ce204 | ||
|
68a0923582 | ||
|
617c801cbd | ||
|
b0c9ce7482 | ||
|
055775b13d | ||
|
8ddf4c9f9f | ||
|
ff40b8f8ab | ||
|
c03344caae | ||
|
237b39a524 |
6
.github/workflows/pr-testing.yml
vendored
6
.github/workflows/pr-testing.yml
vendored
@@ -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'
|
||||
|
@@ -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:{ADE0C9E9-F7D9-4829-8626-8571C735C4D7} 1049:{F5230C0A-9D8C-4C70-AC72-17CECC8273B8} 2052:{D5A0760C-E5B3-4C4C-97B0-81CC445F07B9} 2057:{EF5EF0BE-0B00-4F5C-A2A0-DF2CB82FF20D} " 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.6.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="{F4F774B9-18DC-4740-9552-EA16B98801C9}" 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"/>
|
||||
|
3
pom.xml
3
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.7.0</version>
|
||||
<version>3.8.7</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>
|
||||
|
@@ -211,7 +211,8 @@ public class Account {
|
||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
|
||||
return true;
|
||||
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
// Founders can always mint, unless they have a penalty
|
||||
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
@@ -222,6 +223,11 @@ public class Account {
|
||||
return this.repository.getAccountRepository().getMintedBlockCount(this.address);
|
||||
}
|
||||
|
||||
/** Returns account's blockMintedPenalty or null if account not found in repository. */
|
||||
public Integer getBlocksMintedPenalty() throws DataException {
|
||||
return this.repository.getAccountRepository().getBlocksMintedPenaltyCount(this.address);
|
||||
}
|
||||
|
||||
|
||||
/** Returns whether account can build reward-shares.
|
||||
* <p>
|
||||
@@ -243,7 +249,7 @@ public class Account {
|
||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
|
||||
return true;
|
||||
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
@@ -271,7 +277,7 @@ public class Account {
|
||||
/**
|
||||
* Returns 'effective' minting level, or zero if account does not exist/cannot mint.
|
||||
* <p>
|
||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||
* For founder accounts with no penalty, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||
*
|
||||
* @return 0+
|
||||
* @throws DataException
|
||||
@@ -281,7 +287,8 @@ public class Account {
|
||||
if (accountData == null)
|
||||
return 0;
|
||||
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
// Founders are assigned a different effective minting level, as long as they have no penalty
|
||||
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
|
||||
|
||||
return accountData.getLevel();
|
||||
@@ -289,8 +296,6 @@ public class Account {
|
||||
|
||||
/**
|
||||
* Returns 'effective' minting level, or zero if reward-share does not exist.
|
||||
* <p>
|
||||
* this is being used on src/main/java/org/qortal/api/resource/AddressesResource.java to fulfil the online accounts api call
|
||||
*
|
||||
* @param repository
|
||||
* @param rewardSharePublicKey
|
||||
@@ -309,7 +314,7 @@ public class Account {
|
||||
/**
|
||||
* Returns 'effective' minting level, with a fix for the zero level.
|
||||
* <p>
|
||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||
* For founder accounts with no penalty, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||
*
|
||||
* @param repository
|
||||
* @param rewardSharePublicKey
|
||||
@@ -322,7 +327,7 @@ public class Account {
|
||||
if (rewardShareData == null)
|
||||
return 0;
|
||||
|
||||
else if(!rewardShareData.getMinter().equals(rewardShareData.getRecipient()))//the minter is different than the recipient this means sponsorship
|
||||
else if (!rewardShareData.getMinter().equals(rewardShareData.getRecipient())) // Sponsorship reward share
|
||||
return 0;
|
||||
|
||||
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
||||
|
367
src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java
Normal file
367
src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java
Normal file
@@ -0,0 +1,367 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SelfSponsorshipAlgoV1 {
|
||||
|
||||
private final Repository repository;
|
||||
private final String address;
|
||||
private final AccountData accountData;
|
||||
private final long snapshotTimestamp;
|
||||
private final boolean override;
|
||||
|
||||
private int registeredNameCount = 0;
|
||||
private int suspiciousCount = 0;
|
||||
private int suspiciousPercent = 0;
|
||||
private int consolidationCount = 0;
|
||||
private int bulkIssuanceCount = 0;
|
||||
private int recentSponsorshipCount = 0;
|
||||
|
||||
private List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
private final Map<String, List<TransactionData>> paymentsByAddress = new HashMap<>();
|
||||
private final Set<String> sponsees = new LinkedHashSet<>();
|
||||
private Set<String> consolidatedAddresses = new LinkedHashSet<>();
|
||||
private final Set<String> zeroTransactionAddreses = new LinkedHashSet<>();
|
||||
private final Set<String> penaltyAddresses = new LinkedHashSet<>();
|
||||
|
||||
public SelfSponsorshipAlgoV1(Repository repository, String address, long snapshotTimestamp, boolean override) throws DataException {
|
||||
this.repository = repository;
|
||||
this.address = address;
|
||||
this.accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
this.snapshotTimestamp = snapshotTimestamp;
|
||||
this.override = override;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public Set<String> getPenaltyAddresses() {
|
||||
return this.penaltyAddresses;
|
||||
}
|
||||
|
||||
|
||||
public void run() throws DataException {
|
||||
if (this.accountData == null) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchSponsorshipRewardShares();
|
||||
if (this.sponsorshipRewardShares.isEmpty()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
this.findConsolidatedRewards();
|
||||
this.findBulkIssuance();
|
||||
this.findRegisteredNameCount();
|
||||
this.findRecentSponsorshipCount();
|
||||
|
||||
int score = this.calculateScore();
|
||||
if (score <= 0 && !override) {
|
||||
return;
|
||||
}
|
||||
|
||||
String newAddress = this.getDestinationAccount(this.address);
|
||||
while (newAddress != null) {
|
||||
// Found destination account
|
||||
this.penaltyAddresses.add(newAddress);
|
||||
|
||||
// Run algo for this address, but in "override" mode because it has already been flagged
|
||||
SelfSponsorshipAlgoV1 algoV1 = new SelfSponsorshipAlgoV1(this.repository, newAddress, this.snapshotTimestamp, true);
|
||||
algoV1.run();
|
||||
this.penaltyAddresses.addAll(algoV1.getPenaltyAddresses());
|
||||
|
||||
newAddress = this.getDestinationAccount(newAddress);
|
||||
}
|
||||
|
||||
this.penaltyAddresses.add(this.address);
|
||||
|
||||
if (this.override || this.recentSponsorshipCount < 20) {
|
||||
this.penaltyAddresses.addAll(this.consolidatedAddresses);
|
||||
this.penaltyAddresses.addAll(this.zeroTransactionAddreses);
|
||||
}
|
||||
else {
|
||||
this.penaltyAddresses.addAll(this.sponsees);
|
||||
}
|
||||
}
|
||||
|
||||
private String getDestinationAccount(String address) throws DataException {
|
||||
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
|
||||
if (transferPrivsTransactions.isEmpty()) {
|
||||
// No TRANSFER_PRIVS transactions for this address
|
||||
return null;
|
||||
}
|
||||
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
|
||||
if (accountData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : transferPrivsTransactions) {
|
||||
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||
if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
|
||||
return transferPrivsTransactionData.getRecipient();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void findConsolidatedRewards() throws DataException {
|
||||
List<String> sponseesThatSentRewards = new ArrayList<>();
|
||||
Map<String, Integer> paymentRecipients = new HashMap<>();
|
||||
|
||||
// Collect outgoing payments of each sponsee
|
||||
for (String sponseeAddress : this.sponsees) {
|
||||
|
||||
// Firstly fetch all payments for address, since the functions below depend on this data
|
||||
this.fetchPaymentsForAddress(sponseeAddress);
|
||||
|
||||
// Check if the address has zero relevant transactions
|
||||
if (this.hasZeroTransactions(sponseeAddress)) {
|
||||
this.zeroTransactionAddreses.add(sponseeAddress);
|
||||
}
|
||||
|
||||
// Get payment recipients
|
||||
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||
if (allPaymentRecipients.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
sponseesThatSentRewards.add(sponseeAddress);
|
||||
|
||||
List<String> addressesPaidByThisSponsee = new ArrayList<>();
|
||||
for (String paymentRecipient : allPaymentRecipients) {
|
||||
if (addressesPaidByThisSponsee.contains(paymentRecipient)) {
|
||||
// We already tracked this association - don't allow multiple to stack up
|
||||
continue;
|
||||
}
|
||||
addressesPaidByThisSponsee.add(paymentRecipient);
|
||||
|
||||
// Increment count for this recipient, or initialize to 1 if not present
|
||||
if (paymentRecipients.computeIfPresent(paymentRecipient, (k, v) -> v + 1) == null) {
|
||||
paymentRecipients.put(paymentRecipient, 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Exclude addresses with a low number of payments
|
||||
Map<String, Integer> filteredPaymentRecipients = paymentRecipients.entrySet().stream()
|
||||
.filter(p -> p.getValue() != null && p.getValue() >= 10)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
// Now check how many sponsees have sent to this subset of addresses
|
||||
Map<String, Integer> sponseesThatConsolidatedRewards = new HashMap<>();
|
||||
for (String sponseeAddress : sponseesThatSentRewards) {
|
||||
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||
// Remove any that aren't to one of the flagged recipients (i.e. consolidation)
|
||||
allPaymentRecipients.removeIf(r -> !filteredPaymentRecipients.containsKey(r));
|
||||
|
||||
int count = allPaymentRecipients.size();
|
||||
if (count == 0) {
|
||||
continue;
|
||||
}
|
||||
if (sponseesThatConsolidatedRewards.computeIfPresent(sponseeAddress, (k, v) -> v + count) == null) {
|
||||
sponseesThatConsolidatedRewards.put(sponseeAddress, count);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove sponsees that have only sent a low number of payments to the filtered addresses
|
||||
Map<String, Integer> filteredSponseesThatConsolidatedRewards = sponseesThatConsolidatedRewards.entrySet().stream()
|
||||
.filter(p -> p.getValue() != null && p.getValue() >= 2)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
this.consolidationCount = sponseesThatConsolidatedRewards.size();
|
||||
this.consolidatedAddresses = new LinkedHashSet<>(filteredSponseesThatConsolidatedRewards.keySet());
|
||||
this.suspiciousCount = this.consolidationCount + this.zeroTransactionAddreses.size();
|
||||
this.suspiciousPercent = (int)(this.suspiciousCount / (float) this.sponsees.size() * 100);
|
||||
}
|
||||
|
||||
private void findBulkIssuance() {
|
||||
Long lastTimestamp = null;
|
||||
for (RewardShareTransactionData rewardShareTransactionData : sponsorshipRewardShares) {
|
||||
long timestamp = rewardShareTransactionData.getTimestamp();
|
||||
if (timestamp >= this.snapshotTimestamp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastTimestamp != null) {
|
||||
if (timestamp - lastTimestamp < 3*60*1000L) {
|
||||
this.bulkIssuanceCount++;
|
||||
}
|
||||
}
|
||||
lastTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
private void findRegisteredNameCount() throws DataException {
|
||||
int registeredNameCount = 0;
|
||||
for (String sponseeAddress : sponsees) {
|
||||
List<NameData> names = repository.getNameRepository().getNamesByOwner(sponseeAddress);
|
||||
for (NameData name : names) {
|
||||
if (name.getRegistered() < this.snapshotTimestamp) {
|
||||
registeredNameCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.registeredNameCount = registeredNameCount;
|
||||
}
|
||||
|
||||
private void findRecentSponsorshipCount() {
|
||||
final long referenceTimestamp = this.snapshotTimestamp - (365 * 24 * 60 * 60 * 1000L);
|
||||
int recentSponsorshipCount = 0;
|
||||
for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
|
||||
if (rewardShare.getTimestamp() >= referenceTimestamp) {
|
||||
recentSponsorshipCount++;
|
||||
}
|
||||
}
|
||||
this.recentSponsorshipCount = recentSponsorshipCount;
|
||||
}
|
||||
|
||||
private int calculateScore() {
|
||||
final int suspiciousMultiplier = (this.suspiciousCount >= 100) ? this.suspiciousPercent : 1;
|
||||
final int nameMultiplier = (this.sponsees.size() >= 50 && this.registeredNameCount == 0) ? 2 : 1;
|
||||
final int consolidationMultiplier = Math.max(this.consolidationCount, 1);
|
||||
final int bulkIssuanceMultiplier = Math.max(this.bulkIssuanceCount / 2, 1);
|
||||
final int offset = 9;
|
||||
return suspiciousMultiplier * nameMultiplier * consolidationMultiplier * bulkIssuanceMultiplier - offset;
|
||||
}
|
||||
|
||||
private void fetchSponsorshipRewardShares() throws DataException {
|
||||
List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
|
||||
// Define relevant transactions
|
||||
List<TransactionType> txTypes = List.of(TransactionType.REWARD_SHARE);
|
||||
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, this.address, false);
|
||||
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
if (transactionData.getType() != TransactionType.REWARD_SHARE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
|
||||
|
||||
// Skip removals
|
||||
if (rewardShareTransactionData.getSharePercent() < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not sponsored by this account
|
||||
if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountData.getPublicKey())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip self shares
|
||||
if (Objects.equals(rewardShareTransactionData.getRecipient(), this.address)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean duplicateFound = false;
|
||||
for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
|
||||
if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
|
||||
// Duplicate
|
||||
duplicateFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!duplicateFound) {
|
||||
sponsorshipRewardShares.add(rewardShareTransactionData);
|
||||
this.sponsees.add(rewardShareTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
|
||||
this.sponsorshipRewardShares = sponsorshipRewardShares;
|
||||
}
|
||||
|
||||
private List<TransactionData> fetchTransferPrivsForAddress(String address) throws DataException {
|
||||
return fetchTransactions(repository,
|
||||
List.of(TransactionType.TRANSFER_PRIVS),
|
||||
address, true);
|
||||
}
|
||||
|
||||
private void fetchPaymentsForAddress(String address) throws DataException {
|
||||
List<TransactionData> payments = fetchTransactions(repository,
|
||||
Arrays.asList(TransactionType.PAYMENT, TransactionType.TRANSFER_ASSET),
|
||||
address, false);
|
||||
this.paymentsByAddress.put(address, payments);
|
||||
}
|
||||
|
||||
private List<String> fetchOutgoingPaymentRecipientsForAddress(String address) {
|
||||
List<String> outgoingPaymentRecipients = new ArrayList<>();
|
||||
|
||||
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||
if (transactionDataList == null) transactionDataList = new ArrayList<>();
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
switch (transactionData.getType()) {
|
||||
|
||||
case PAYMENT:
|
||||
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
|
||||
if (!Objects.equals(paymentTransactionData.getRecipient(), address)) {
|
||||
// Outgoing payment from this account
|
||||
outgoingPaymentRecipients.add(paymentTransactionData.getRecipient());
|
||||
}
|
||||
break;
|
||||
|
||||
case TRANSFER_ASSET:
|
||||
TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
|
||||
if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
|
||||
if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
|
||||
// Outgoing payment from this account
|
||||
outgoingPaymentRecipients.add(transferAssetTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return outgoingPaymentRecipients;
|
||||
}
|
||||
|
||||
private boolean hasZeroTransactions(String address) {
|
||||
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||
if (transactionDataList == null) {
|
||||
return true;
|
||||
}
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
|
||||
return transactionDataList.size() == 0;
|
||||
}
|
||||
|
||||
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
|
||||
// Fetch all relevant transactions for this account
|
||||
List<byte[]> signatures = repository.getTransactionRepository()
|
||||
.getSignaturesMatchingCriteria(null, null, null, txTypes,
|
||||
null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
|
||||
null, null, reverse);
|
||||
|
||||
List<TransactionData> transactionDataList = new ArrayList<>();
|
||||
|
||||
for (byte[] signature : signatures) {
|
||||
// Fetch transaction data
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData == null) {
|
||||
continue;
|
||||
}
|
||||
transactionDataList.add(transactionData);
|
||||
}
|
||||
|
||||
return transactionDataList;
|
||||
}
|
||||
|
||||
}
|
56
src/main/java/org/qortal/api/model/AccountPenaltyStats.java
Normal file
56
src/main/java/org/qortal/api/model/AccountPenaltyStats.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import org.qortal.block.SelfSponsorshipAlgoV1Block;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class AccountPenaltyStats {
|
||||
|
||||
public Integer totalPenalties;
|
||||
public Integer maxPenalty;
|
||||
public Integer minPenalty;
|
||||
public String penaltyHash;
|
||||
|
||||
protected AccountPenaltyStats() {
|
||||
}
|
||||
|
||||
public AccountPenaltyStats(Integer totalPenalties, Integer maxPenalty, Integer minPenalty, String penaltyHash) {
|
||||
this.totalPenalties = totalPenalties;
|
||||
this.maxPenalty = maxPenalty;
|
||||
this.minPenalty = minPenalty;
|
||||
this.penaltyHash = penaltyHash;
|
||||
}
|
||||
|
||||
public static AccountPenaltyStats fromAccounts(List<AccountData> accounts) {
|
||||
int totalPenalties = 0;
|
||||
Integer maxPenalty = null;
|
||||
Integer minPenalty = null;
|
||||
|
||||
List<String> addresses = new ArrayList<>();
|
||||
for (AccountData accountData : accounts) {
|
||||
int penalty = accountData.getBlocksMintedPenalty();
|
||||
addresses.add(accountData.getAddress());
|
||||
totalPenalties++;
|
||||
|
||||
// Penalties are expressed as a negative number, so the min and the max are reversed here
|
||||
if (maxPenalty == null || penalty < maxPenalty) maxPenalty = penalty;
|
||||
if (minPenalty == null || penalty > minPenalty) minPenalty = penalty;
|
||||
}
|
||||
|
||||
String penaltyHash = SelfSponsorshipAlgoV1Block.getHash(addresses);
|
||||
return new AccountPenaltyStats(totalPenalties, maxPenalty, minPenalty, penaltyHash);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("totalPenalties: %d, maxPenalty: %d, minPenalty: %d, penaltyHash: %s", totalPenalties, maxPenalty, minPenalty, penaltyHash == null ? "null" : penaltyHash);
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.network.Handshake;
|
||||
@@ -36,6 +37,7 @@ public class ConnectedPeer {
|
||||
public Long lastBlockTimestamp;
|
||||
public UUID connectionId;
|
||||
public String age;
|
||||
public Boolean isTooDivergent;
|
||||
|
||||
protected ConnectedPeer() {
|
||||
}
|
||||
@@ -69,6 +71,11 @@ public class ConnectedPeer {
|
||||
this.lastBlockSignature = peerChainTipData.getSignature();
|
||||
this.lastBlockTimestamp = peerChainTipData.getTimestamp();
|
||||
}
|
||||
|
||||
// Only include isTooDivergent decision if we've had the opportunity to request block summaries this peer
|
||||
if (peer.getLastTooDivergentTime() != null) {
|
||||
this.isTooDivergent = Controller.wasRecentlyTooDivergent.test(peer);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
@@ -27,6 +28,7 @@ import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.AccountPenaltyStats;
|
||||
import org.qortal.api.model.ApiOnlineAccount;
|
||||
import org.qortal.api.model.RewardShareKeyRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
@@ -34,6 +36,7 @@ import org.qortal.controller.LiteNode;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.AccountPenaltyData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.network.OnlineAccountLevel;
|
||||
@@ -471,6 +474,54 @@ public class AddressesResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/penalties")
|
||||
@Operation(
|
||||
summary = "Get addresses with penalties",
|
||||
description = "Returns a list of accounts with a blocksMintedPenalty",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "accounts with penalties",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AccountPenaltyData.class)))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public List<AccountPenaltyData> getAccountsWithPenalties() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||
List<AccountPenaltyData> penalties = accounts.stream().map(a -> new AccountPenaltyData(a.getAddress(), a.getBlocksMintedPenalty())).collect(Collectors.toList());
|
||||
|
||||
return penalties;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/penalties/stats")
|
||||
@Operation(
|
||||
summary = "Get stats about current penalties",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "aggregated stats about accounts with penalties",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AccountPenaltyStats.class)))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public AccountPenaltyStats getPenaltyStats() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||
return AccountPenaltyStats.fromAccounts(accounts);
|
||||
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/publicize")
|
||||
@Operation(
|
||||
|
@@ -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(
|
||||
|
@@ -719,7 +719,7 @@ public class ArbitraryResource {
|
||||
try {
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, false);
|
||||
if (transactionMetadata != null) {
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, true);
|
||||
if (resourceMetadata != null) {
|
||||
return resourceMetadata;
|
||||
}
|
||||
@@ -1128,7 +1128,7 @@ public class ArbitraryResource {
|
||||
if (path == null) {
|
||||
// See if we have a string instead
|
||||
if (string != null) {
|
||||
File tempFile = File.createTempFile("qortal-", ".tmp");
|
||||
File tempFile = File.createTempFile("qortal-", "");
|
||||
tempFile.deleteOnExit();
|
||||
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toPath().toString()));
|
||||
writer.write(string);
|
||||
@@ -1138,7 +1138,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
// ... or base64 encoded raw data
|
||||
else if (base64 != null) {
|
||||
File tempFile = File.createTempFile("qortal-", ".tmp");
|
||||
File tempFile = File.createTempFile("qortal-", "");
|
||||
tempFile.deleteOnExit();
|
||||
Files.write(tempFile.toPath(), Base64.decode(base64));
|
||||
path = tempFile.toPath().toString();
|
||||
@@ -1288,7 +1288,7 @@ public class ArbitraryResource {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, false);
|
||||
if (resourceMetadata != null) {
|
||||
resourceInfo.metadata = resourceMetadata;
|
||||
}
|
||||
|
@@ -634,13 +634,16 @@ public class BlocksResource {
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<BlockData> getBlockRange(@PathParam("height") int height, @Parameter(
|
||||
ref = "count"
|
||||
) @QueryParam("count") int count) {
|
||||
public List<BlockData> getBlockRange(@PathParam("height") int height,
|
||||
@Parameter(ref = "count") @QueryParam("count") int count,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
|
||||
@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<BlockData> blocks = new ArrayList<>();
|
||||
boolean shouldReverse = (reverse != null && reverse == true);
|
||||
|
||||
for (/* count already set */; count > 0; --count, ++height) {
|
||||
int i = 0;
|
||||
while (i < count) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null) {
|
||||
// Not found - try the archive
|
||||
@@ -650,8 +653,14 @@ public class BlocksResource {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
|
||||
blocks.add(blockData);
|
||||
|
||||
height = shouldReverse ? height - 1 : height + 1;
|
||||
i++;
|
||||
}
|
||||
|
||||
return blocks;
|
||||
|
@@ -60,7 +60,7 @@ public class BootstrapResource {
|
||||
bootstrap.validateBlockchain();
|
||||
return bootstrap.create();
|
||||
|
||||
} catch (DataException | InterruptedException | IOException e) {
|
||||
} catch (Exception e) {
|
||||
LOGGER.info("Unable to create bootstrap", e);
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||
}
|
||||
|
@@ -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(
|
||||
@@ -68,7 +100,7 @@ public class CrossChainBitcoinResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
Long balance = bitcoin.getWalletBalanceFromTransactions(key58);
|
||||
Long balance = bitcoin.getWalletBalance(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
@@ -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(
|
||||
|
@@ -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(
|
||||
@@ -68,7 +100,7 @@ public class CrossChainDigibyteResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
Long balance = digibyte.getWalletBalanceFromTransactions(key58);
|
||||
Long balance = digibyte.getWalletBalance(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
@@ -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(
|
||||
|
@@ -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(
|
||||
@@ -66,7 +98,7 @@ public class CrossChainDogecoinResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
Long balance = dogecoin.getWalletBalanceFromTransactions(key58);
|
||||
Long balance = dogecoin.getWalletBalance(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
@@ -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(
|
||||
|
@@ -8,11 +8,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
@@ -25,7 +24,6 @@ import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.qortal.api.*;
|
||||
import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
@@ -586,98 +584,103 @@ public class CrossChainHtlcResource {
|
||||
}
|
||||
|
||||
List<TradeBotData> allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
|
||||
TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
|
||||
if (tradeBotData == null)
|
||||
List<TradeBotData> tradeBotDataList = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).collect(Collectors.toList());
|
||||
if (tradeBotDataList == null || tradeBotDataList.isEmpty())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||
int lockTime = tradeBotData.getLockTimeA();
|
||||
// Loop through all matching entries for this AT address, as there might be more than one
|
||||
for (TradeBotData tradeBotData : tradeBotDataList) {
|
||||
|
||||
// We can't refund P2SH-A until lockTime-A has passed
|
||||
if (NTP.getTime() <= lockTime * 1000L)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
||||
if (tradeBotData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = bitcoiny.getMedianBlockTime();
|
||||
if (medianBlockTime <= lockTime)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
||||
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||
int lockTime = tradeBotData.getLockTimeA();
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
// We can't refund P2SH-A until lockTime-A has passed
|
||||
if (NTP.getTime() <= lockTime * 1000L)
|
||||
continue;
|
||||
|
||||
// Create redeem script based on destination chain
|
||||
byte[] redeemScriptA;
|
||||
String p2shAddressA;
|
||||
BitcoinyHTLC.Status htlcStatusA;
|
||||
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
|
||||
redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
p2shAddressA = PirateChain.getInstance().deriveP2shAddressBPrefix(redeemScriptA);
|
||||
htlcStatusA = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
}
|
||||
else {
|
||||
redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
||||
htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
}
|
||||
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = bitcoiny.getMedianBlockTime();
|
||||
if (medianBlockTime <= lockTime)
|
||||
continue;
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
// Still waiting for P2SH-A to be funded...
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
// Too late!
|
||||
return false;
|
||||
// Create redeem script based on destination chain
|
||||
byte[] redeemScriptA;
|
||||
String p2shAddressA;
|
||||
BitcoinyHTLC.Status htlcStatusA;
|
||||
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
|
||||
redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
p2shAddressA = PirateChain.getInstance().deriveP2shAddressBPrefix(redeemScriptA);
|
||||
htlcStatusA = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
} else {
|
||||
redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
||||
htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
}
|
||||
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
|
||||
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
// Still waiting for P2SH-A to be funded...
|
||||
continue;
|
||||
|
||||
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
|
||||
// Pirate Chain custom integration
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
// Too late!
|
||||
continue;
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA);
|
||||
case FUNDED: {
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
|
||||
// Get funding txid
|
||||
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
if (fundingTxidHex == null) {
|
||||
throw new ForeignBlockchainException("Missing funding txid when refunding P2SH");
|
||||
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
|
||||
// Pirate Chain custom integration
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Get funding txid
|
||||
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
if (fundingTxidHex == null) {
|
||||
throw new ForeignBlockchainException("Missing funding txid when refunding P2SH");
|
||||
}
|
||||
String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
|
||||
|
||||
byte[] privateKey = tradeBotData.getTradePrivateKey();
|
||||
String privateKey58 = Base58.encode(privateKey);
|
||||
String redeemScript58 = Base58.encode(redeemScriptA);
|
||||
|
||||
String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
|
||||
receiveAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTime, privateKey58);
|
||||
LOGGER.info("Refund txid: {}", txid);
|
||||
} else {
|
||||
// ElectrumX coins
|
||||
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
// Validate the destination foreign blockchain address
|
||||
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
|
||||
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
|
||||
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
|
||||
|
||||
bitcoiny.broadcastTransaction(p2shRefundTransaction);
|
||||
}
|
||||
String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
|
||||
|
||||
byte[] privateKey = tradeBotData.getTradePrivateKey();
|
||||
String privateKey58 = Base58.encode(privateKey);
|
||||
String redeemScript58 = Base58.encode(redeemScriptA);
|
||||
|
||||
String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
|
||||
receiveAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTime, privateKey58);
|
||||
LOGGER.info("Refund txid: {}", txid);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// ElectrumX coins
|
||||
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
// Validate the destination foreign blockchain address
|
||||
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
|
||||
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
|
||||
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
|
||||
|
||||
bitcoiny.broadcastTransaction(p2shRefundTransaction);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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(
|
||||
@@ -68,7 +100,7 @@ public class CrossChainLitecoinResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
Long balance = litecoin.getWalletBalanceFromTransactions(key58);
|
||||
Long balance = litecoin.getWalletBalance(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
@@ -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(
|
||||
|
@@ -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(
|
||||
|
@@ -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(
|
||||
@@ -68,7 +100,7 @@ public class CrossChainRavencoinResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
Long balance = ravencoin.getWalletBalanceFromTransactions(key58);
|
||||
Long balance = ravencoin.getWalletBalance(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
@@ -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(
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.arbitrary;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataMetadataCache;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
@@ -88,7 +89,7 @@ public class ArbitraryDataBuilder {
|
||||
if (latestPut == null) {
|
||||
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
||||
this.name, this.service, this.identifierString());
|
||||
throw new DataException(message);
|
||||
throw new DataNotPublishedException(message);
|
||||
}
|
||||
this.latestPutTransaction = latestPut;
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
||||
@@ -59,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) {
|
||||
@@ -115,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
|
||||
*
|
||||
@@ -162,6 +171,8 @@ public class ArbitraryDataReader {
|
||||
return;
|
||||
}
|
||||
|
||||
this.arbitraryDataResource = this.createArbitraryDataResource();
|
||||
|
||||
this.preExecute();
|
||||
this.deleteExistingFiles();
|
||||
this.fetch();
|
||||
@@ -169,10 +180,18 @@ public class ArbitraryDataReader {
|
||||
this.uncompress();
|
||||
this.validate();
|
||||
|
||||
} catch (DataNotPublishedException e) {
|
||||
if (e.getMessage() != null) {
|
||||
// Log the message only, to avoid spamming the logs with a full stack trace
|
||||
LOGGER.debug("DataNotPublishedException when trying to load QDN resource: {}", e.getMessage());
|
||||
}
|
||||
this.deleteWorkingDirectory();
|
||||
throw e;
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("DataException when trying to load QDN resource", e);
|
||||
this.deleteWorkingDirectory();
|
||||
throw new DataException(e.getMessage());
|
||||
throw e;
|
||||
|
||||
} finally {
|
||||
this.postExecute();
|
||||
@@ -427,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());
|
||||
@@ -438,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 {
|
||||
|
@@ -3,6 +3,7 @@ package org.qortal.arbitrary;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
||||
@@ -325,7 +326,7 @@ public class ArbitraryDataResource {
|
||||
if (latestPut == null) {
|
||||
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
||||
this.resourceId, this.service, this.identifierString());
|
||||
throw new DataException(message);
|
||||
throw new DataNotPublishedException(message);
|
||||
}
|
||||
this.latestPutTransaction = latestPut;
|
||||
|
||||
|
@@ -23,16 +23,13 @@ import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.*;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ArbitraryDataWriter {
|
||||
|
||||
@@ -50,6 +47,7 @@ public class ArbitraryDataWriter {
|
||||
private final String description;
|
||||
private final List<String> tags;
|
||||
private final Category category;
|
||||
private List<String> files;
|
||||
|
||||
private int chunkSize = ArbitraryDataFile.CHUNK_SIZE;
|
||||
|
||||
@@ -80,12 +78,14 @@ public class ArbitraryDataWriter {
|
||||
this.description = ArbitraryDataTransactionMetadata.limitDescription(description);
|
||||
this.tags = ArbitraryDataTransactionMetadata.limitTags(tags);
|
||||
this.category = category;
|
||||
this.files = new ArrayList<>(); // Populated in buildFileList()
|
||||
}
|
||||
|
||||
public void save() throws IOException, DataException, InterruptedException, MissingDataException {
|
||||
try {
|
||||
this.preExecute();
|
||||
this.validateService();
|
||||
this.buildFileList();
|
||||
this.process();
|
||||
this.compress();
|
||||
this.encrypt();
|
||||
@@ -143,6 +143,24 @@ public class ArbitraryDataWriter {
|
||||
}
|
||||
}
|
||||
|
||||
private void buildFileList() throws IOException {
|
||||
// Single file resources consist of a single element in the file list
|
||||
boolean isSingleFile = this.filePath.toFile().isFile();
|
||||
if (isSingleFile) {
|
||||
this.files.add(this.filePath.getFileName().toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Multi file resources require a walk through the directory tree
|
||||
try (Stream<Path> stream = Files.walk(this.filePath)) {
|
||||
this.files = stream
|
||||
.filter(Files::isRegularFile)
|
||||
.map(p -> this.filePath.relativize(p).toString())
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
private void process() throws DataException, IOException, MissingDataException {
|
||||
switch (this.method) {
|
||||
|
||||
@@ -285,6 +303,7 @@ public class ArbitraryDataWriter {
|
||||
metadata.setTags(this.tags);
|
||||
metadata.setCategory(this.category);
|
||||
metadata.setChunks(this.arbitraryDataFile.chunkHashList());
|
||||
metadata.setFiles(this.files);
|
||||
metadata.write();
|
||||
|
||||
// Create an ArbitraryDataFile from the JSON file (we don't have a signature yet)
|
||||
|
@@ -0,0 +1,22 @@
|
||||
package org.qortal.arbitrary.exception;
|
||||
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
public class DataNotPublishedException extends DataException {
|
||||
|
||||
public DataNotPublishedException() {
|
||||
}
|
||||
|
||||
public DataNotPublishedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DataNotPublishedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public DataNotPublishedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
@@ -19,6 +19,7 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
private String description;
|
||||
private List<String> tags;
|
||||
private Category category;
|
||||
private List<String> files;
|
||||
|
||||
private static int MAX_TITLE_LENGTH = 80;
|
||||
private static int MAX_DESCRIPTION_LENGTH = 500;
|
||||
@@ -77,6 +78,20 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
}
|
||||
this.chunks = chunksList;
|
||||
}
|
||||
|
||||
List<String> filesList = new ArrayList<>();
|
||||
if (metadata.has("files")) {
|
||||
JSONArray files = metadata.getJSONArray("files");
|
||||
if (files != null) {
|
||||
for (int i=0; i<files.length(); i++) {
|
||||
String tag = files.getString(i);
|
||||
if (tag != null) {
|
||||
filesList.add(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.files = filesList;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,6 +126,14 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
}
|
||||
outer.put("chunks", chunks);
|
||||
|
||||
JSONArray files = new JSONArray();
|
||||
if (this.files != null) {
|
||||
for (String file : this.files) {
|
||||
files.put(file);
|
||||
}
|
||||
}
|
||||
outer.put("files", files);
|
||||
|
||||
this.jsonString = outer.toString(2);
|
||||
LOGGER.trace("Transaction metadata: {}", this.jsonString);
|
||||
}
|
||||
@@ -156,6 +179,14 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
return this.category;
|
||||
}
|
||||
|
||||
public void setFiles(List<String> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public List<String> getFiles() {
|
||||
return this.files;
|
||||
}
|
||||
|
||||
public boolean containsChunk(byte[] chunk) {
|
||||
for (byte[] c : this.chunks) {
|
||||
if (Arrays.equals(c, chunk)) {
|
||||
|
@@ -10,9 +10,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
@@ -20,9 +18,52 @@ import static java.util.stream.Collectors.toMap;
|
||||
public enum Service {
|
||||
AUTO_UPDATE(1, false, null, null),
|
||||
ARBITRARY_DATA(100, false, null, null),
|
||||
QCHAT_ATTACHMENT(120, true, 1024*1024L, null) {
|
||||
@Override
|
||||
public ValidationResult validate(Path path) throws IOException {
|
||||
ValidationResult superclassResult = super.validate(path);
|
||||
if (superclassResult != ValidationResult.OK) {
|
||||
return superclassResult;
|
||||
}
|
||||
|
||||
// Custom validation function to require a single file, with a whitelisted extension
|
||||
int fileCount = 0;
|
||||
File[] files = path.toFile().listFiles();
|
||||
// If already a single file, replace the list with one that contains that file only
|
||||
if (files == null && path.toFile().isFile()) {
|
||||
files = new File[] { path.toFile() };
|
||||
}
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.getName().equals(".qortal")) {
|
||||
continue;
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
|
||||
}
|
||||
final String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
|
||||
// We must allow blank file extensions because these are used by data published from a plaintext or base64-encoded string
|
||||
final List<String> allowedExtensions = Arrays.asList("zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "");
|
||||
if (extension == null || !allowedExtensions.contains(extension)) {
|
||||
return ValidationResult.INVALID_FILE_EXTENSION;
|
||||
}
|
||||
fileCount++;
|
||||
}
|
||||
}
|
||||
if (fileCount != 1) {
|
||||
return ValidationResult.INVALID_FILE_COUNT;
|
||||
}
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
},
|
||||
WEBSITE(200, true, null, null) {
|
||||
@Override
|
||||
public ValidationResult validate(Path path) {
|
||||
public ValidationResult validate(Path path) throws IOException {
|
||||
ValidationResult superclassResult = super.validate(path);
|
||||
if (superclassResult != ValidationResult.OK) {
|
||||
return superclassResult;
|
||||
}
|
||||
|
||||
// Custom validation function to require an index HTML file in the root directory
|
||||
List<String> fileNames = ArbitraryDataRenderer.indexFiles();
|
||||
String[] files = path.toFile().list();
|
||||
@@ -43,6 +84,8 @@ public enum Service {
|
||||
QCHAT_IMAGE(420, true, 500*1024L, null),
|
||||
VIDEO(500, false, null, null),
|
||||
AUDIO(600, false, null, null),
|
||||
QCHAT_AUDIO(610, true, 10*1024*1024L, null),
|
||||
QCHAT_VOICE(620, true, 10*1024*1024L, null),
|
||||
BLOG(700, false, null, null),
|
||||
BLOG_POST(777, false, null, null),
|
||||
BLOG_COMMENT(778, false, null, null),
|
||||
@@ -53,12 +96,24 @@ public enum Service {
|
||||
METADATA(1100, false, null, null),
|
||||
GIF_REPOSITORY(1200, true, 25*1024*1024L, null) {
|
||||
@Override
|
||||
public ValidationResult validate(Path path) {
|
||||
public ValidationResult validate(Path path) throws IOException {
|
||||
ValidationResult superclassResult = super.validate(path);
|
||||
if (superclassResult != ValidationResult.OK) {
|
||||
return superclassResult;
|
||||
}
|
||||
|
||||
// Custom validation function to require .gif files only, and at least 1
|
||||
int gifCount = 0;
|
||||
File[] files = path.toFile().listFiles();
|
||||
// If already a single file, replace the list with one that contains that file only
|
||||
if (files == null && path.toFile().isFile()) {
|
||||
files = new File[] { path.toFile() };
|
||||
}
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.getName().equals(".qortal")) {
|
||||
continue;
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
|
||||
}
|
||||
@@ -143,7 +198,8 @@ public enum Service {
|
||||
MISSING_INDEX_FILE(4),
|
||||
DIRECTORIES_NOT_ALLOWED(5),
|
||||
INVALID_FILE_EXTENSION(6),
|
||||
MISSING_DATA(7);
|
||||
MISSING_DATA(7),
|
||||
INVALID_FILE_COUNT(8);
|
||||
|
||||
public final int value;
|
||||
|
||||
|
@@ -136,7 +136,7 @@ public class Block {
|
||||
}
|
||||
|
||||
/** Lazy-instantiated expanded info on block's online accounts. */
|
||||
private static class ExpandedAccount {
|
||||
public static class ExpandedAccount {
|
||||
private final RewardShareData rewardShareData;
|
||||
private final int sharePercent;
|
||||
private final boolean isRecipientAlsoMinter;
|
||||
@@ -169,6 +169,13 @@ public class Block {
|
||||
}
|
||||
}
|
||||
|
||||
public Account getMintingAccount() {
|
||||
return this.mintingAccount;
|
||||
}
|
||||
public Account getRecipientAccount() {
|
||||
return this.recipientAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns share bin for expanded account.
|
||||
* <p>
|
||||
@@ -363,12 +370,26 @@ public class Block {
|
||||
return null;
|
||||
}
|
||||
|
||||
int height = parentBlockData.getHeight() + 1;
|
||||
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
|
||||
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
|
||||
|
||||
// Fetch our list of online accounts, removing any that are missing a nonce
|
||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
|
||||
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
|
||||
|
||||
// After feature trigger, remove any online accounts that are level 0
|
||||
if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||
onlineAccounts.removeIf(a -> {
|
||||
try {
|
||||
return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0;
|
||||
} catch (DataException e) {
|
||||
// Something went wrong, so remove the account
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
LOGGER.debug("No online accounts - not even our own?");
|
||||
return null;
|
||||
@@ -435,7 +456,6 @@ public class Block {
|
||||
|
||||
int transactionCount = 0;
|
||||
byte[] transactionsSignature = null;
|
||||
int height = parentBlockData.getHeight() + 1;
|
||||
|
||||
int atCount = 0;
|
||||
long atFees = 0;
|
||||
@@ -1029,6 +1049,15 @@ public class Block {
|
||||
if (onlineRewardShares == null)
|
||||
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
|
||||
|
||||
// After feature trigger, require all online account minters to be greater than level 0
|
||||
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||
for (ExpandedAccount account : expandedAccounts) {
|
||||
if (account.getMintingAccount().getEffectiveMintingLevel() == 0)
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
// If block is past a certain age then we simply assume the signatures were correct
|
||||
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
|
||||
if (this.blockData.getTimestamp() < signatureRequirementThreshold)
|
||||
@@ -1434,6 +1463,9 @@ public class Block {
|
||||
if (this.blockData.getHeight() == 212937)
|
||||
// Apply fix for block 212937
|
||||
Block212937.processFix(this);
|
||||
|
||||
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
|
||||
SelfSponsorshipAlgoV1Block.processAccountPenalties(this);
|
||||
}
|
||||
|
||||
// We're about to (test-)process a batch of transactions,
|
||||
@@ -1490,19 +1522,23 @@ public class Block {
|
||||
// Batch update in repository
|
||||
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1);
|
||||
|
||||
// Keep track of level bumps in case we need to apply to other entries
|
||||
Map<String, Integer> bumpedAccounts = new HashMap<>();
|
||||
|
||||
// Local changes and also checks for level bump
|
||||
for (AccountData accountData : allUniqueExpandedAccounts) {
|
||||
// Adjust count locally (in Java)
|
||||
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
||||
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
if (newLevel > accountData.getLevel()) {
|
||||
// Account has increased in level!
|
||||
accountData.setLevel(newLevel);
|
||||
bumpedAccounts.put(accountData.getAddress(), newLevel);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
LOGGER.trace(() -> String.format("Block minter %s bumped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
}
|
||||
@@ -1510,6 +1546,25 @@ public class Block {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Also bump other entries if need be
|
||||
if (!bumpedAccounts.isEmpty()) {
|
||||
for (ExpandedAccount expandedAccount : expandedAccounts) {
|
||||
Integer newLevel = bumpedAccounts.get(expandedAccount.mintingAccountData.getAddress());
|
||||
if (newLevel != null && expandedAccount.mintingAccountData.getLevel() != newLevel) {
|
||||
expandedAccount.mintingAccountData.setLevel(newLevel);
|
||||
LOGGER.trace("Also bumped {} to level {}", expandedAccount.mintingAccountData.getAddress(), newLevel);
|
||||
}
|
||||
|
||||
if (!expandedAccount.isRecipientAlsoMinter) {
|
||||
newLevel = bumpedAccounts.get(expandedAccount.recipientAccountData.getAddress());
|
||||
if (newLevel != null && expandedAccount.recipientAccountData.getLevel() != newLevel) {
|
||||
expandedAccount.recipientAccountData.setLevel(newLevel);
|
||||
LOGGER.trace("Also bumped {} to level {}", expandedAccount.recipientAccountData.getAddress(), newLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void processBlockRewards() throws DataException {
|
||||
@@ -1669,6 +1724,9 @@ public class Block {
|
||||
// Revert fix for block 212937
|
||||
Block212937.orphanFix(this);
|
||||
|
||||
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
|
||||
SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
|
||||
|
||||
// Block rewards, including transaction fees, removed after transactions undone
|
||||
orphanBlockRewards();
|
||||
|
||||
@@ -1797,7 +1855,7 @@ public class Block {
|
||||
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
|
||||
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
|
@@ -75,6 +75,9 @@ public class BlockChain {
|
||||
transactionV6Timestamp,
|
||||
disableReferenceTimestamp,
|
||||
increaseOnlineAccountsDifficultyTimestamp,
|
||||
onlineAccountMinterLevelValidationHeight,
|
||||
selfSponsorshipAlgoV1Height,
|
||||
feeValidationFixTimestamp,
|
||||
chatReferenceTimestamp;
|
||||
}
|
||||
|
||||
@@ -97,6 +100,13 @@ public class BlockChain {
|
||||
/** Whether only one registered name is allowed per account. */
|
||||
private boolean oneNamePerAccount = false;
|
||||
|
||||
/** Checkpoints */
|
||||
public static class Checkpoint {
|
||||
public int height;
|
||||
public String signature;
|
||||
}
|
||||
private List<Checkpoint> checkpoints;
|
||||
|
||||
/** Block rewards by block height */
|
||||
public static class RewardByHeight {
|
||||
public int height;
|
||||
@@ -197,6 +207,9 @@ public class BlockChain {
|
||||
* featureTriggers because unit tests need to set this value via Reflection. */
|
||||
private long onlineAccountsModulusV2Timestamp;
|
||||
|
||||
/** Snapshot timestamp for self sponsorship algo V1 */
|
||||
private long selfSponsorshipAlgoV1SnapshotTimestamp;
|
||||
|
||||
/** Max reward shares by block height */
|
||||
public static class MaxRewardSharesByTimestamp {
|
||||
public long timestamp;
|
||||
@@ -357,6 +370,11 @@ public class BlockChain {
|
||||
return this.onlineAccountsModulusV2Timestamp;
|
||||
}
|
||||
|
||||
// Self sponsorship algo
|
||||
public long getSelfSponsorshipAlgoV1SnapshotTimestamp() {
|
||||
return this.selfSponsorshipAlgoV1SnapshotTimestamp;
|
||||
}
|
||||
|
||||
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
|
||||
public boolean getRequireGroupForApproval() {
|
||||
return this.requireGroupForApproval;
|
||||
@@ -370,6 +388,10 @@ public class BlockChain {
|
||||
return this.oneNamePerAccount;
|
||||
}
|
||||
|
||||
public List<Checkpoint> getCheckpoints() {
|
||||
return this.checkpoints;
|
||||
}
|
||||
|
||||
public List<RewardByHeight> getBlockRewardsByHeight() {
|
||||
return this.rewardsByHeight;
|
||||
}
|
||||
@@ -484,6 +506,18 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.increaseOnlineAccountsDifficultyTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public int getSelfSponsorshipAlgoV1Height() {
|
||||
return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV1Height.name()).intValue();
|
||||
}
|
||||
|
||||
public long getOnlineAccountMinterLevelValidationHeight() {
|
||||
return this.featureTriggers.get(FeatureTrigger.onlineAccountMinterLevelValidationHeight.name()).intValue();
|
||||
}
|
||||
|
||||
public long getFeeValidationFixTimestamp() {
|
||||
return this.featureTriggers.get(FeatureTrigger.feeValidationFixTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public long getChatReferenceTimestamp() {
|
||||
return this.featureTriggers.get(FeatureTrigger.chatReferenceTimestamp.name()).longValue();
|
||||
}
|
||||
@@ -656,6 +690,7 @@ public class BlockChain {
|
||||
|
||||
boolean isTopOnly = Settings.getInstance().isTopOnly();
|
||||
boolean archiveEnabled = Settings.getInstance().isArchiveEnabled();
|
||||
boolean isLite = Settings.getInstance().isLite();
|
||||
boolean canBootstrap = Settings.getInstance().getBootstrap();
|
||||
boolean needsArchiveRebuild = false;
|
||||
BlockData chainTip;
|
||||
@@ -676,22 +711,44 @@ public class BlockChain {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checkpoints
|
||||
// Limited to topOnly nodes for now, in order to reduce risk, and to solve a real-world problem with divergent topOnly nodes
|
||||
// TODO: remove the isTopOnly conditional below once this feature has had more testing time
|
||||
if (isTopOnly && !isLite) {
|
||||
List<Checkpoint> checkpoints = BlockChain.getInstance().getCheckpoints();
|
||||
for (Checkpoint checkpoint : checkpoints) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(checkpoint.height);
|
||||
if (blockData == null) {
|
||||
// Try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(checkpoint.height);
|
||||
}
|
||||
if (blockData == null) {
|
||||
LOGGER.trace("Couldn't find block for height {}", checkpoint.height);
|
||||
// This is likely due to the block being pruned, so is safe to ignore.
|
||||
// Continue, as there might be other blocks we can check more definitively.
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] signature = Base58.decode(checkpoint.signature);
|
||||
if (!Arrays.equals(signature, blockData.getSignature())) {
|
||||
LOGGER.info("Error: block at height {} with signature {} doesn't match checkpoint sig: {}. Bootstrapping...", checkpoint.height, Base58.encode(blockData.getSignature()), checkpoint.signature);
|
||||
needsArchiveRebuild = true;
|
||||
break;
|
||||
}
|
||||
LOGGER.info("Block at height {} matches checkpoint signature", blockData.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
boolean hasBlocks = (chainTip != null && chainTip.getHeight() > 1);
|
||||
// Check first block is Genesis Block
|
||||
if (!isGenesisBlockValid() || needsArchiveRebuild) {
|
||||
try {
|
||||
rebuildBlockchain();
|
||||
|
||||
if (isTopOnly && hasBlocks) {
|
||||
// Top-only mode is enabled and we have blocks, so it's possible that the genesis block has been pruned
|
||||
// It's best not to validate it, and there's no real need to
|
||||
} else {
|
||||
// Check first block is Genesis Block
|
||||
if (!isGenesisBlockValid() || needsArchiveRebuild) {
|
||||
try {
|
||||
rebuildBlockchain();
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage()));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,9 +757,7 @@ public class BlockChain {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
repository.checkConsistency();
|
||||
|
||||
// Set the number of blocks to validate based on the pruned state of the chain
|
||||
// If pruned, subtract an extra 10 to allow room for error
|
||||
int blocksToValidate = (isTopOnly || archiveEnabled) ? Settings.getInstance().getPruneBlockLimit() - 10 : 1440;
|
||||
int blocksToValidate = Math.min(Settings.getInstance().getPruneBlockLimit() - 10, 1440);
|
||||
|
||||
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - blocksToValidate, 1);
|
||||
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
|
||||
|
133
src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
Normal file
133
src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
Normal file
@@ -0,0 +1,133 @@
|
||||
package org.qortal.block;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.SelfSponsorshipAlgoV1;
|
||||
import org.qortal.api.model.AccountPenaltyStats;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.AccountPenaltyData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Self Sponsorship AlgoV1 Block
|
||||
* <p>
|
||||
* Selected block for the initial run on the "self sponsorship detection algorithm"
|
||||
*/
|
||||
public final class SelfSponsorshipAlgoV1Block {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV1Block.class);
|
||||
|
||||
|
||||
private SelfSponsorshipAlgoV1Block() {
|
||||
/* Do not instantiate */
|
||||
}
|
||||
|
||||
public static void processAccountPenalties(Block block) throws DataException {
|
||||
LOGGER.info("Running algo for block processing - this will take a while...");
|
||||
logPenaltyStats(block.repository);
|
||||
long startTime = System.currentTimeMillis();
|
||||
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, -5000000);
|
||||
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||
long totalTime = System.currentTimeMillis() - startTime;
|
||||
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||
LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||
logPenaltyStats(block.repository);
|
||||
|
||||
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||
}
|
||||
|
||||
public static void orphanAccountPenalties(Block block) throws DataException {
|
||||
LOGGER.info("Running algo for block orphaning - this will take a while...");
|
||||
logPenaltyStats(block.repository);
|
||||
long startTime = System.currentTimeMillis();
|
||||
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, 5000000);
|
||||
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||
long totalTime = System.currentTimeMillis() - startTime;
|
||||
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||
LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||
logPenaltyStats(block.repository);
|
||||
|
||||
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||
}
|
||||
|
||||
public static Set<AccountPenaltyData> getAccountPenalties(Repository repository, int penalty) throws DataException {
|
||||
final long snapshotTimestamp = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
|
||||
Set<AccountPenaltyData> penalties = new LinkedHashSet<>();
|
||||
List<String> addresses = repository.getTransactionRepository().getConfirmedRewardShareCreatorsExcludingSelfShares();
|
||||
for (String address : addresses) {
|
||||
//System.out.println(String.format("address: %s", address));
|
||||
SelfSponsorshipAlgoV1 selfSponsorshipAlgoV1 = new SelfSponsorshipAlgoV1(repository, address, snapshotTimestamp, false);
|
||||
selfSponsorshipAlgoV1.run();
|
||||
//System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV1.getPenaltyAddresses().size()));
|
||||
|
||||
for (String penaltyAddress : selfSponsorshipAlgoV1.getPenaltyAddresses()) {
|
||||
penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
|
||||
}
|
||||
}
|
||||
return penalties;
|
||||
}
|
||||
|
||||
private static int updateAccountLevels(Repository repository, Set<AccountPenaltyData> accountPenalties) throws DataException {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
int updatedCount = 0;
|
||||
|
||||
for (AccountPenaltyData penaltyData : accountPenalties) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress());
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
// Shortcut for penalties
|
||||
if (effectiveBlocksMinted < 0) {
|
||||
accountData.setLevel(0);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
updatedCount++;
|
||||
LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) {
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
accountData.setLevel(newLevel);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
updatedCount++;
|
||||
LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
private static void logPenaltyStats(Repository repository) {
|
||||
try {
|
||||
LOGGER.info(getPenaltyStats(repository));
|
||||
|
||||
} catch (DataException e) {}
|
||||
}
|
||||
|
||||
private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException {
|
||||
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||
return AccountPenaltyStats.fromAccounts(accounts);
|
||||
}
|
||||
|
||||
public static String getHash(List<String> penaltyAddresses) {
|
||||
if (penaltyAddresses == null || penaltyAddresses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Collections.sort(penaltyAddresses);
|
||||
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
}
|
@@ -26,9 +26,6 @@ import org.qortal.data.block.CommonBlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.BlockSummariesV2Message;
|
||||
import org.qortal.network.message.HeightV2Message;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.repository.BlockRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -38,6 +35,8 @@ import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
// Minting new blocks
|
||||
|
||||
public class BlockMinter extends Thread {
|
||||
@@ -64,8 +63,8 @@ public class BlockMinter extends Thread {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("BlockMinter");
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes do not mint
|
||||
if (Settings.getInstance().isTopOnly() || Settings.getInstance().isLite()) {
|
||||
// Top only and lite nodes do not sign blocks
|
||||
return;
|
||||
}
|
||||
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
||||
@@ -511,6 +510,21 @@ public class BlockMinter extends Thread {
|
||||
|
||||
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
||||
|
||||
Block block = mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
||||
assertNotNull("Minted block must not be null", block);
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
public static Block mintTestingBlockUnvalidated(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
|
||||
if (!BlockChain.getInstance().isTestChain())
|
||||
throw new DataException("Ignoring attempt to mint testing block for non-test chain!");
|
||||
|
||||
// Ensure mintingAccount is 'online' so blocks can be minted
|
||||
OnlineAccountsManager.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
|
||||
|
||||
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
||||
|
||||
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
||||
}
|
||||
|
||||
@@ -518,6 +532,8 @@ public class BlockMinter extends Thread {
|
||||
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
||||
if (newBlock == null)
|
||||
return null;
|
||||
|
||||
// Make sure we're the only thread modifying the blockchain
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
@@ -29,6 +29,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.ApiService;
|
||||
import org.qortal.api.DomainMapService;
|
||||
import org.qortal.api.GatewayService;
|
||||
@@ -756,6 +757,28 @@ public class Controller extends Thread {
|
||||
return peer.isAtLeastVersion(minPeerVersion) == false;
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> hasInvalidSigner = peer -> {
|
||||
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
if (peerChainTipData == null)
|
||||
return true;
|
||||
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
return Account.getRewardShareEffectiveMintingLevel(repository, peerChainTipData.getMinterPublicKey()) == 0;
|
||||
} catch (DataException e) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> wasRecentlyTooDivergent = peer -> {
|
||||
Long now = NTP.getTime();
|
||||
Long peerLastTooDivergentTime = peer.getLastTooDivergentTime();
|
||||
if (now == null || peerLastTooDivergentTime == null)
|
||||
return false;
|
||||
|
||||
// Exclude any peers that were TOO_DIVERGENT in the last 5 mins
|
||||
return (now - peerLastTooDivergentTime < 5 * 60 * 1000L);
|
||||
};
|
||||
|
||||
private long getRandomRepositoryMaintenanceInterval() {
|
||||
final long minInterval = Settings.getInstance().getRepositoryMaintenanceMinInterval();
|
||||
final long maxInterval = Settings.getInstance().getRepositoryMaintenanceMaxInterval();
|
||||
|
@@ -247,6 +247,9 @@ public class Synchronizer extends Thread {
|
||||
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
||||
peers.removeIf(Controller.hasInferiorChainTip);
|
||||
|
||||
// Disregard peers that have a block with an invalid signer
|
||||
peers.removeIf(Controller.hasInvalidSigner);
|
||||
|
||||
final int peersBeforeComparison = peers.size();
|
||||
|
||||
// Request recent block summaries from the remaining peers, and locate our common block with each
|
||||
@@ -1118,6 +1121,7 @@ public class Synchronizer extends Thread {
|
||||
// If common block is too far behind us then we're on massively different forks so give up.
|
||||
if (!force && testHeight < ourHeight - MAXIMUM_COMMON_DELTA) {
|
||||
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
|
||||
peer.setLastTooDivergentTime(NTP.getTime());
|
||||
return SynchronizationResult.TOO_DIVERGENT;
|
||||
}
|
||||
|
||||
@@ -1127,6 +1131,9 @@ public class Synchronizer extends Thread {
|
||||
testHeight = Math.max(testHeight - step, 1);
|
||||
}
|
||||
|
||||
// Peer not considered too divergent
|
||||
peer.setLastTooDivergentTime(0L);
|
||||
|
||||
// Prepend test block's summary as first block summary, as summaries returned are *after* test block
|
||||
BlockSummaryData testBlockSummary = new BlockSummaryData(testBlockData);
|
||||
blockSummariesFromCommon.add(0, testBlockSummary);
|
||||
|
@@ -82,7 +82,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
|
||||
try {
|
||||
// Use a fixed thread pool to execute the arbitrary data file requests
|
||||
int threadCount = 10;
|
||||
int threadCount = 5;
|
||||
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
|
||||
@@ -288,7 +288,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
// The ID needs to match that of the original request
|
||||
message.setId(originalMessage.getId());
|
||||
|
||||
if (!requestingPeer.sendMessage(message)) {
|
||||
if (!requestingPeer.sendMessageWithTimeout(message, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
|
||||
LOGGER.debug("Failed to forward arbitrary data file to peer {}", requestingPeer);
|
||||
requestingPeer.disconnect("failed to forward arbitrary data file");
|
||||
}
|
||||
@@ -564,13 +564,16 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
LOGGER.trace("Hash {} exists", hash58);
|
||||
|
||||
// We can serve the file directly as we already have it
|
||||
LOGGER.debug("Sending file {}...", arbitraryDataFile);
|
||||
ArbitraryDataFileMessage arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, arbitraryDataFile);
|
||||
arbitraryDataFileMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(arbitraryDataFileMessage)) {
|
||||
LOGGER.debug("Couldn't sent file");
|
||||
if (!peer.sendMessageWithTimeout(arbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
|
||||
LOGGER.debug("Couldn't send file {}", arbitraryDataFile);
|
||||
peer.disconnect("failed to send file");
|
||||
}
|
||||
LOGGER.debug("Sent file {}", arbitraryDataFile);
|
||||
else {
|
||||
LOGGER.debug("Sent file {}", arbitraryDataFile);
|
||||
}
|
||||
}
|
||||
else if (relayInfo != null) {
|
||||
LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -39,9 +39,11 @@ 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()) {
|
||||
repository.discardChanges();
|
||||
@@ -91,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;
|
||||
|
@@ -26,9 +26,11 @@ 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()) {
|
||||
repository.discardChanges();
|
||||
@@ -69,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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
@@ -357,19 +367,33 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
* @return unspent BTC balance, or null if unable to determine balance
|
||||
*/
|
||||
public Long getWalletBalance(String key58) throws ForeignBlockchainException {
|
||||
// It's more accurate to calculate the balance from the transactions, rather than asking Bitcoinj
|
||||
return this.getWalletBalanceFromTransactions(key58);
|
||||
Long balance = 0L;
|
||||
|
||||
// Context.propagate(bitcoinjContext);
|
||||
//
|
||||
// Wallet wallet = walletFromDeterministicKey58(key58);
|
||||
// wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
|
||||
//
|
||||
// Coin balance = wallet.getBalance();
|
||||
// if (balance == null)
|
||||
// return null;
|
||||
//
|
||||
// return balance.value;
|
||||
List<TransactionOutput> allUnspentOutputs = new ArrayList<>();
|
||||
Set<String> walletAddresses = this.getWalletAddresses(key58);
|
||||
for (String address : walletAddresses) {
|
||||
allUnspentOutputs.addAll(this.getUnspentOutputs(address));
|
||||
}
|
||||
for (TransactionOutput output : allUnspentOutputs) {
|
||||
if (!output.isAvailableForSpending()) {
|
||||
continue;
|
||||
}
|
||||
balance += output.getValue().value;
|
||||
}
|
||||
return balance;
|
||||
}
|
||||
|
||||
public Long getWalletBalanceFromBitcoinj(String key58) {
|
||||
Context.propagate(bitcoinjContext);
|
||||
|
||||
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
|
||||
|
||||
Coin balance = wallet.getBalance();
|
||||
if (balance == null)
|
||||
return null;
|
||||
|
||||
return balance.value;
|
||||
}
|
||||
|
||||
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
|
||||
@@ -464,6 +488,64 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getWalletAddresses(String key58) throws ForeignBlockchainException {
|
||||
synchronized (this) {
|
||||
Context.propagate(bitcoinjContext);
|
||||
|
||||
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
||||
|
||||
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
|
||||
keyChain.maybeLookAhead();
|
||||
|
||||
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
|
||||
|
||||
Set<String> keySet = new HashSet<>();
|
||||
|
||||
int unusedCounter = 0;
|
||||
int ki = 0;
|
||||
do {
|
||||
boolean areAllKeysUnused = true;
|
||||
|
||||
for (; ki < keys.size(); ++ki) {
|
||||
DeterministicKey dKey = keys.get(ki);
|
||||
|
||||
// Check for transactions
|
||||
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||
keySet.add(address.toString());
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
|
||||
// Ask for transaction history - if it's empty then key has never been used
|
||||
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, false);
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
areAllKeysUnused = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (areAllKeysUnused) {
|
||||
// No transactions
|
||||
if (unusedCounter >= Settings.getInstance().getGapLimit()) {
|
||||
// ... and we've hit our search limit
|
||||
break;
|
||||
}
|
||||
// We haven't hit our search limit yet so increment the counter and keep looking
|
||||
unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
|
||||
} else {
|
||||
// Some keys in this batch were used, so reset the counter
|
||||
unusedCounter = 0;
|
||||
}
|
||||
|
||||
// Generate some more keys
|
||||
keys.addAll(generateMoreKeys(keyChain));
|
||||
|
||||
// Process new keys
|
||||
} while (true);
|
||||
|
||||
return keySet;
|
||||
}
|
||||
}
|
||||
|
||||
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
|
||||
long amount = 0;
|
||||
long total = 0L;
|
||||
|
@@ -45,6 +45,9 @@ 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.qortal.link", Server.ConnectionType.SSL, 55002),
|
||||
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));
|
||||
@@ -134,6 +137,8 @@ public class Digibyte extends Bitcoiny {
|
||||
Context bitcoinjContext = new Context(digibyteNet.getParams());
|
||||
|
||||
instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||
|
||||
electrumX.setBlockchain(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
|
@@ -45,10 +45,13 @@ 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.qortal.link", Server.ConnectionType.SSL, 54002),
|
||||
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
|
||||
|
@@ -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.'})"
|
||||
@@ -40,7 +45,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
|
||||
|
||||
private static final int RESPONSE_TIME_READINGS = 5;
|
||||
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
|
||||
private static final long MAX_AVG_RESPONSE_TIME = 1000L; // ms
|
||||
|
||||
public static class Server {
|
||||
String hostname;
|
||||
@@ -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";
|
||||
|
@@ -45,15 +45,19 @@ 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.qortal.link", Server.ConnectionType.SSL, 50002),
|
||||
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));
|
||||
}
|
||||
|
||||
|
@@ -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));
|
||||
}
|
||||
|
||||
|
@@ -117,7 +117,7 @@ public class PirateWallet {
|
||||
// Restore existing wallet
|
||||
String response = LiteWalletJni.initfromb64(serverUri, params, wallet, saplingOutput64, saplingSpend64);
|
||||
if (response != null && !response.contains("\"initalized\":true")) {
|
||||
LOGGER.info("Unable to initialize Pirate Chain wallet: {}", response);
|
||||
LOGGER.info("Unable to initialize Pirate Chain wallet at {}: {}", serverUri, response);
|
||||
return false;
|
||||
}
|
||||
this.seedPhrase = inputSeedPhrase;
|
||||
|
@@ -45,13 +45,17 @@ 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.qortal.link", Server.ConnectionType.SSL, 56002),
|
||||
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
|
||||
@@ -138,6 +142,8 @@ public class Ravencoin extends Bitcoiny {
|
||||
Context bitcoinjContext = new Context(ravencoinNet.getParams());
|
||||
|
||||
instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||
|
||||
electrumX.setBlockchain(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
|
@@ -18,6 +18,7 @@ public class AccountData {
|
||||
protected int level;
|
||||
protected int blocksMinted;
|
||||
protected int blocksMintedAdjustment;
|
||||
protected int blocksMintedPenalty;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -25,7 +26,7 @@ public class AccountData {
|
||||
protected AccountData() {
|
||||
}
|
||||
|
||||
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int level, int blocksMinted, int blocksMintedAdjustment) {
|
||||
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int level, int blocksMinted, int blocksMintedAdjustment, int blocksMintedPenalty) {
|
||||
this.address = address;
|
||||
this.reference = reference;
|
||||
this.publicKey = publicKey;
|
||||
@@ -34,10 +35,11 @@ public class AccountData {
|
||||
this.level = level;
|
||||
this.blocksMinted = blocksMinted;
|
||||
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
||||
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||
}
|
||||
|
||||
public AccountData(String address) {
|
||||
this(address, null, null, Group.NO_GROUP, 0, 0, 0, 0);
|
||||
this(address, null, null, Group.NO_GROUP, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
@@ -102,6 +104,14 @@ public class AccountData {
|
||||
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
||||
}
|
||||
|
||||
public int getBlocksMintedPenalty() {
|
||||
return this.blocksMintedPenalty;
|
||||
}
|
||||
|
||||
public void setBlocksMintedPenalty(int blocksMintedPenalty) {
|
||||
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||
}
|
||||
|
||||
// Comparison
|
||||
|
||||
@Override
|
||||
|
@@ -0,0 +1,52 @@
|
||||
package org.qortal.data.account;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class AccountPenaltyData {
|
||||
|
||||
// Properties
|
||||
private String address;
|
||||
private int blocksMintedPenalty;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAXB
|
||||
protected AccountPenaltyData() {
|
||||
}
|
||||
|
||||
public AccountPenaltyData(String address, int blocksMintedPenalty) {
|
||||
this.address = address;
|
||||
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public int getBlocksMintedPenalty() {
|
||||
return this.blocksMintedPenalty;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s has penalty %d", this.address, this.blocksMintedPenalty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object b) {
|
||||
if (!(b instanceof AccountPenaltyData))
|
||||
return false;
|
||||
|
||||
return this.getAddress().equals(((AccountPenaltyData) b).getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return address.hashCode();
|
||||
}
|
||||
|
||||
}
|
@@ -15,22 +15,24 @@ public class ArbitraryResourceMetadata {
|
||||
private List<String> tags;
|
||||
private Category category;
|
||||
private String categoryName;
|
||||
private List<String> files;
|
||||
|
||||
public ArbitraryResourceMetadata() {
|
||||
}
|
||||
|
||||
public ArbitraryResourceMetadata(String title, String description, List<String> tags, Category category) {
|
||||
public ArbitraryResourceMetadata(String title, String description, List<String> tags, Category category, List<String> files) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.tags = tags;
|
||||
this.category = category;
|
||||
this.files = files;
|
||||
|
||||
if (category != null) {
|
||||
this.categoryName = category.getName();
|
||||
}
|
||||
}
|
||||
|
||||
public static ArbitraryResourceMetadata fromTransactionMetadata(ArbitraryDataTransactionMetadata transactionMetadata) {
|
||||
public static ArbitraryResourceMetadata fromTransactionMetadata(ArbitraryDataTransactionMetadata transactionMetadata, boolean includeFileList) {
|
||||
if (transactionMetadata == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -39,10 +41,20 @@ public class ArbitraryResourceMetadata {
|
||||
List<String> tags = transactionMetadata.getTags();
|
||||
Category category = transactionMetadata.getCategory();
|
||||
|
||||
if (title == null && description == null && tags == null && category == null) {
|
||||
// We don't always want to include the file list as it can be too verbose
|
||||
List<String> files = null;
|
||||
if (includeFileList) {
|
||||
files = transactionMetadata.getFiles();
|
||||
}
|
||||
|
||||
if (title == null && description == null && tags == null && category == null && files == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ArbitraryResourceMetadata(title, description, tags, category);
|
||||
return new ArbitraryResourceMetadata(title, description, tags, category, files);
|
||||
}
|
||||
|
||||
public List<String> getFiles() {
|
||||
return this.files;
|
||||
}
|
||||
}
|
||||
|
@@ -17,17 +17,21 @@ public class ActiveChats {
|
||||
private Long timestamp;
|
||||
private String sender;
|
||||
private String senderName;
|
||||
private byte[] signature;
|
||||
private byte[] data;
|
||||
|
||||
protected GroupChat() {
|
||||
/* JAXB */
|
||||
}
|
||||
|
||||
public GroupChat(int groupId, String groupName, Long timestamp, String sender, String senderName) {
|
||||
public GroupChat(int groupId, String groupName, Long timestamp, String sender, String senderName, byte[] signature, byte[] data) {
|
||||
this.groupId = groupId;
|
||||
this.groupName = groupName;
|
||||
this.timestamp = timestamp;
|
||||
this.sender = sender;
|
||||
this.senderName = senderName;
|
||||
this.signature = signature;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int getGroupId() {
|
||||
@@ -49,6 +53,14 @@ public class ActiveChats {
|
||||
public String getSenderName() {
|
||||
return this.senderName;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@@ -118,4 +130,4 @@ public class ActiveChats {
|
||||
return this.direct;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -85,6 +85,10 @@ public class ChatTransactionData extends TransactionData {
|
||||
return this.chatReference;
|
||||
}
|
||||
|
||||
public void setChatReference(byte[] chatReference) {
|
||||
this.chatReference = chatReference;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.data.transaction;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
@@ -90,4 +91,17 @@ public class DeployAtTransactionData extends TransactionData {
|
||||
this.aTAddress = AtAddress;
|
||||
}
|
||||
|
||||
// Re-expose creatorPublicKey for this transaction type for JAXB
|
||||
@XmlElement(name = "creatorPublicKey")
|
||||
@Schema(name = "creatorPublicKey", description = "AT creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||
public byte[] getAtCreatorPublicKey() {
|
||||
return this.creatorPublicKey;
|
||||
}
|
||||
|
||||
@XmlElement(name = "creatorPublicKey")
|
||||
@Schema(name = "creatorPublicKey", description = "AT creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||
public void setAtCreatorPublicKey(byte[] creatorPublicKey) {
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -265,7 +265,7 @@ public enum Handshake {
|
||||
private static final long PEER_VERSION_131 = 0x0100030001L;
|
||||
|
||||
/** Minimum peer version that we are allowed to communicate with */
|
||||
private static final String MIN_PEER_VERSION = "3.1.0";
|
||||
private static final String MIN_PEER_VERSION = "3.8.2";
|
||||
|
||||
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
||||
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
|
||||
|
@@ -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);
|
||||
|
@@ -155,6 +155,11 @@ public class Peer {
|
||||
*/
|
||||
private CommonBlockData commonBlockData;
|
||||
|
||||
/**
|
||||
* Last time we detected this peer as TOO_DIVERGENT
|
||||
*/
|
||||
private Long lastTooDivergentTime;
|
||||
|
||||
// Message stats
|
||||
|
||||
private static class MessageStats {
|
||||
@@ -383,6 +388,14 @@ public class Peer {
|
||||
this.commonBlockData = commonBlockData;
|
||||
}
|
||||
|
||||
public Long getLastTooDivergentTime() {
|
||||
return this.lastTooDivergentTime;
|
||||
}
|
||||
|
||||
public void setLastTooDivergentTime(Long lastTooDivergentTime) {
|
||||
this.lastTooDivergentTime = lastTooDivergentTime;
|
||||
}
|
||||
|
||||
public boolean isSyncInProgress() {
|
||||
return this.syncInProgress;
|
||||
}
|
||||
|
@@ -41,6 +41,8 @@ public class AccountMessage extends Message {
|
||||
|
||||
bytes.write(Ints.toByteArray(accountData.getBlocksMintedAdjustment()));
|
||||
|
||||
bytes.write(Ints.toByteArray(accountData.getBlocksMintedPenalty()));
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
@@ -80,7 +82,9 @@ public class AccountMessage extends Message {
|
||||
|
||||
int blocksMintedAdjustment = byteBuffer.getInt();
|
||||
|
||||
AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment);
|
||||
int blocksMintedPenalty = byteBuffer.getInt();
|
||||
|
||||
AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty);
|
||||
return new AccountMessage(id, accountData);
|
||||
}
|
||||
|
||||
|
@@ -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. */
|
||||
|
@@ -1,13 +1,9 @@
|
||||
package org.qortal.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.EligibleQoraHolderData;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.QortFromQoraData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.account.*;
|
||||
|
||||
public interface AccountRepository {
|
||||
|
||||
@@ -19,6 +15,9 @@ public interface AccountRepository {
|
||||
/** Returns accounts with <b>any</b> bit set in given mask. */
|
||||
public List<AccountData> getFlaggedAccounts(int mask) throws DataException;
|
||||
|
||||
/** Returns accounts with a blockedMintedPenalty */
|
||||
public List<AccountData> getPenaltyAccounts() throws DataException;
|
||||
|
||||
/** Returns account's last reference or null if not set or account not found. */
|
||||
public byte[] getLastReference(String address) throws DataException;
|
||||
|
||||
@@ -100,6 +99,18 @@ public interface AccountRepository {
|
||||
*/
|
||||
public void modifyMintedBlockCounts(List<String> addresses, int delta) throws DataException;
|
||||
|
||||
/** Returns account's block minted penalty count or null if account not found. */
|
||||
public Integer getBlocksMintedPenaltyCount(String address) throws DataException;
|
||||
|
||||
/**
|
||||
* Sets blocks minted penalties for given list of accounts.
|
||||
* This replaces the existing values rather than modifying them by a delta.
|
||||
*
|
||||
* @param accountPenalties
|
||||
* @throws DataException
|
||||
*/
|
||||
public void updateBlocksMintedPenalties(Set<AccountPenaltyData> accountPenalties) throws DataException;
|
||||
|
||||
/** Delete account from repository. */
|
||||
public void delete(String address) throws DataException;
|
||||
|
||||
|
@@ -279,7 +279,9 @@ public class Bootstrap {
|
||||
|
||||
LOGGER.info("Generating checksum file...");
|
||||
String checksum = Crypto.digestHexString(compressedOutputPath.toFile(), 1024*1024);
|
||||
LOGGER.info("checksum: {}", checksum);
|
||||
Path checksumPath = Paths.get(String.format("%s.sha256", compressedOutputPath.toString()));
|
||||
LOGGER.info("Writing checksum to path: {}", checksumPath);
|
||||
Files.writeString(checksumPath, checksum, StandardOpenOption.CREATE);
|
||||
|
||||
// Return the path to the compressed bootstrap file
|
||||
|
@@ -131,7 +131,14 @@ public interface GroupRepository {
|
||||
|
||||
public GroupBanData getBan(int groupId, String member) throws DataException;
|
||||
|
||||
public boolean banExists(int groupId, String offender) throws DataException;
|
||||
/**
|
||||
* IMPORTANT: when using banExists() as part of validation, the timestamp must be that of the transaction that
|
||||
* is calling banExists() as part of its validation. It must NOT be the current time, unless this is being
|
||||
* called outside of validation, as part of an on demand check for a ban existing (such as via an API call).
|
||||
* This is because we need to evaluate a ban's status based on the time of the subsequent transaction, as
|
||||
* validation will not occur at a fixed time for every node. For some, it could be months into the future.
|
||||
*/
|
||||
public boolean banExists(int groupId, String offender, long timestamp) throws DataException;
|
||||
|
||||
public List<GroupBanData> getGroupBans(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
|
@@ -179,6 +179,15 @@ public interface TransactionRepository {
|
||||
public List<TransferAssetTransactionData> getAssetTransfers(long assetId, String address, Integer limit, Integer offset, Boolean reverse)
|
||||
throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of reward share transaction creators, excluding self shares.
|
||||
* This uses confirmed transactions only.
|
||||
*
|
||||
* @return
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<String> getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of transactions pending approval, with optional txGgroupId filtering.
|
||||
* <p>
|
||||
|
@@ -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);
|
||||
|
@@ -6,15 +6,11 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.EligibleQoraHolderData;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.QortFromQoraData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.account.*;
|
||||
import org.qortal.repository.AccountRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
@@ -30,7 +26,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public AccountData getAccount(String address) throws DataException {
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment FROM Accounts WHERE account = ?";
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty FROM Accounts WHERE account = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
|
||||
if (resultSet == null)
|
||||
@@ -43,8 +39,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
int level = resultSet.getInt(5);
|
||||
int blocksMinted = resultSet.getInt(6);
|
||||
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||
int blocksMintedPenalty = resultSet.getInt(8);
|
||||
|
||||
return new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment);
|
||||
return new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch account info from repository", e);
|
||||
}
|
||||
@@ -52,7 +49,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public List<AccountData> getFlaggedAccounts(int mask) throws DataException {
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, account FROM Accounts WHERE BITAND(flags, ?) != 0";
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty, account FROM Accounts WHERE BITAND(flags, ?) != 0";
|
||||
|
||||
List<AccountData> accounts = new ArrayList<>();
|
||||
|
||||
@@ -68,9 +65,10 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
int level = resultSet.getInt(5);
|
||||
int blocksMinted = resultSet.getInt(6);
|
||||
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||
String address = resultSet.getString(8);
|
||||
int blocksMintedPenalty = resultSet.getInt(8);
|
||||
String address = resultSet.getString(9);
|
||||
|
||||
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment));
|
||||
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty));
|
||||
} while (resultSet.next());
|
||||
|
||||
return accounts;
|
||||
@@ -79,6 +77,36 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AccountData> getPenaltyAccounts() throws DataException {
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty, account FROM Accounts WHERE blocks_minted_penalty != 0";
|
||||
|
||||
List<AccountData> accounts = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return accounts;
|
||||
|
||||
do {
|
||||
byte[] reference = resultSet.getBytes(1);
|
||||
byte[] publicKey = resultSet.getBytes(2);
|
||||
int defaultGroupId = resultSet.getInt(3);
|
||||
int flags = resultSet.getInt(4);
|
||||
int level = resultSet.getInt(5);
|
||||
int blocksMinted = resultSet.getInt(6);
|
||||
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||
int blocksMintedPenalty = resultSet.getInt(8);
|
||||
String address = resultSet.getString(9);
|
||||
|
||||
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty));
|
||||
} while (resultSet.next());
|
||||
|
||||
return accounts;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch penalty accounts from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getLastReference(String address) throws DataException {
|
||||
String sql = "SELECT reference FROM Accounts WHERE account = ?";
|
||||
@@ -298,6 +326,39 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getBlocksMintedPenaltyCount(String address) throws DataException {
|
||||
String sql = "SELECT blocks_minted_penalty FROM Accounts WHERE account = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
return resultSet.getInt(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch account's block minted penalty count from repository", e);
|
||||
}
|
||||
}
|
||||
public void updateBlocksMintedPenalties(Set<AccountPenaltyData> accountPenalties) throws DataException {
|
||||
// Nothing to do?
|
||||
if (accountPenalties == null || accountPenalties.isEmpty())
|
||||
return;
|
||||
|
||||
// Map balance changes into SQL bind params, filtering out no-op changes
|
||||
List<Object[]> updateBlocksMintedPenaltyParams = accountPenalties.stream()
|
||||
.map(accountPenalty -> new Object[] { accountPenalty.getAddress(), accountPenalty.getBlocksMintedPenalty(), accountPenalty.getBlocksMintedPenalty() })
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Perform actual balance changes
|
||||
String sql = "INSERT INTO Accounts (account, blocks_minted_penalty) VALUES (?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE blocks_minted_penalty = blocks_minted_penalty + ?";
|
||||
try {
|
||||
this.repository.executeCheckedBatchUpdate(sql, updateBlocksMintedPenaltyParams);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to set blocks minted penalties in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String address) throws DataException {
|
||||
// NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
|
||||
|
@@ -177,11 +177,11 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
|
||||
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 "
|
||||
String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name, signature, data "
|
||||
+ "FROM GroupMembers "
|
||||
+ "JOIN Groups USING (group_id) "
|
||||
+ "LEFT OUTER JOIN LATERAL("
|
||||
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name "
|
||||
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name, signature, data "
|
||||
+ "FROM ChatTransactions "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
||||
@@ -205,8 +205,10 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
|
||||
String sender = resultSet.getString(4);
|
||||
String senderName = resultSet.getString(5);
|
||||
byte[] signature = resultSet.getBytes(6);
|
||||
byte[] data = resultSet.getBytes(7);
|
||||
|
||||
GroupChat groupChat = new GroupChat(groupId, groupName, timestamp, sender, senderName);
|
||||
GroupChat groupChat = new GroupChat(groupId, groupName, timestamp, sender, senderName, signature, data);
|
||||
groupChats.add(groupChat);
|
||||
} while (resultSet.next());
|
||||
}
|
||||
@@ -215,7 +217,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
}
|
||||
|
||||
// We need different SQL to handle group-less chat
|
||||
String grouplessSql = "SELECT created_when, sender, SenderNames.name "
|
||||
String grouplessSql = "SELECT created_when, sender, SenderNames.name, signature, data "
|
||||
+ "FROM ChatTransactions "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
||||
@@ -228,15 +230,19 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
Long timestamp = null;
|
||||
String sender = null;
|
||||
String senderName = null;
|
||||
byte[] signature = null;
|
||||
byte[] data = 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);
|
||||
signature = resultSet.getBytes(4);
|
||||
data = resultSet.getBytes(5);
|
||||
}
|
||||
|
||||
GroupChat groupChat = new GroupChat(0, null, timestamp, sender, senderName);
|
||||
GroupChat groupChat = new GroupChat(0, null, timestamp, sender, senderName, signature, data);
|
||||
groupChats.add(groupChat);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch active group chats from repository", e);
|
||||
@@ -291,4 +297,4 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
return directChats;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -976,9 +976,21 @@ public class HSQLDBDatabaseUpdates {
|
||||
break;
|
||||
|
||||
case 44:
|
||||
// Add blocks minted penalty
|
||||
stmt.execute("ALTER TABLE Accounts ADD blocks_minted_penalty INTEGER NOT NULL DEFAULT 0");
|
||||
break;
|
||||
|
||||
case 45:
|
||||
// Add a chat reference, to allow one message to reference another, and for this to be easily
|
||||
// searchable. Null values are allowed as most transactions won't have a reference.
|
||||
stmt.execute("ALTER TABLE ChatTransactions ADD chat_reference Signature");
|
||||
// For finding chat messages by reference
|
||||
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:
|
||||
|
@@ -777,9 +777,9 @@ public class HSQLDBGroupRepository implements GroupRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean banExists(int groupId, String offender) throws DataException {
|
||||
public boolean banExists(int groupId, String offender, long timestamp) throws DataException {
|
||||
try {
|
||||
return this.repository.exists("GroupBans", "group_id = ? AND offender = ?", groupId, offender);
|
||||
return this.repository.exists("GroupBans", "group_id = ? AND offender = ? AND (expires_when IS NULL OR expires_when > ?)", groupId, offender, timestamp);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to check for group ban in repository", e);
|
||||
}
|
||||
|
@@ -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<>();
|
||||
|
@@ -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);
|
||||
|
@@ -7,11 +7,7 @@ import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -969,6 +965,33 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException {
|
||||
List<String> rewardShareCreators = new ArrayList<>();
|
||||
|
||||
String sql = "SELECT account "
|
||||
+ "FROM RewardShareTransactions "
|
||||
+ "JOIN Accounts ON Accounts.public_key = RewardShareTransactions.minter_public_key "
|
||||
+ "JOIN Transactions ON Transactions.signature = RewardShareTransactions.signature "
|
||||
+ "WHERE block_height IS NOT NULL AND RewardShareTransactions.recipient != Accounts.account "
|
||||
+ "GROUP BY account "
|
||||
+ "ORDER BY account";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return rewardShareCreators;
|
||||
|
||||
do {
|
||||
String address = resultSet.getString(1);
|
||||
|
||||
rewardShareCreators.add(address);
|
||||
} while (resultSet.next());
|
||||
|
||||
return rewardShareCreators;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch reward share creators from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TransactionData> getApprovalPendingTransactions(Integer txGroupId, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
|
@@ -110,7 +110,13 @@ public class Settings {
|
||||
/** Maximum number of unconfirmed transactions allowed per account */
|
||||
private int maxUnconfirmedPerAccount = 25;
|
||||
/** Max milliseconds into future for accepting new, unconfirmed transactions */
|
||||
private int maxTransactionTimestampFuture = 24 * 60 * 60 * 1000; // milliseconds
|
||||
private int maxTransactionTimestampFuture = 30 * 60 * 1000; // milliseconds
|
||||
|
||||
/** Maximum number of CHAT transactions allowed per account in recent timeframe */
|
||||
private int maxRecentChatMessagesPerAccount = 250;
|
||||
/** Maximum age of a CHAT transaction to be considered 'recent' */
|
||||
private long recentChatMessagesMaxAge = 60 * 60 * 1000L; // milliseconds
|
||||
|
||||
/** Whether we check, fetch and install auto-updates */
|
||||
private boolean autoUpdateEnabled = true;
|
||||
/** How long between repository backups (ms), or 0 if disabled. */
|
||||
@@ -153,7 +159,7 @@ public class Settings {
|
||||
* This prevents the node from being able to serve older blocks */
|
||||
private boolean topOnly = false;
|
||||
/** The amount of recent blocks we should keep when pruning */
|
||||
private int pruneBlockLimit = 1450;
|
||||
private int pruneBlockLimit = 6000;
|
||||
|
||||
/** How often to attempt AT state pruning (ms). */
|
||||
private long atStatesPruneInterval = 3219L; // milliseconds
|
||||
@@ -209,7 +215,7 @@ public class Settings {
|
||||
public long recoveryModeTimeout = 10 * 60 * 1000L;
|
||||
|
||||
/** Minimum peer version number required in order to sync with them */
|
||||
private String minPeerVersion = "3.6.3";
|
||||
private String minPeerVersion = "3.8.2";
|
||||
/** Whether to allow connections with peers below minPeerVersion
|
||||
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
||||
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||
@@ -267,7 +273,7 @@ public class Settings {
|
||||
private String[] bootstrapHosts = new String[] {
|
||||
"http://bootstrap.qortal.org",
|
||||
"http://bootstrap2.qortal.org",
|
||||
"http://62.171.190.193"
|
||||
"http://bootstrap.qortal.online"
|
||||
};
|
||||
|
||||
// Auto-update sources
|
||||
@@ -640,6 +646,14 @@ public class Settings {
|
||||
return this.maxTransactionTimestampFuture;
|
||||
}
|
||||
|
||||
public int getMaxRecentChatMessagesPerAccount() {
|
||||
return this.maxRecentChatMessagesPerAccount;
|
||||
}
|
||||
|
||||
public long getRecentChatMessagesMaxAge() {
|
||||
return recentChatMessagesMaxAge;
|
||||
}
|
||||
|
||||
public int getBlockCacheSize() {
|
||||
return this.blockCacheSize;
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class ArbitraryTransaction extends Transaction {
|
||||
|
||||
@@ -34,9 +35,13 @@ public class ArbitraryTransaction extends Transaction {
|
||||
public static final int MAX_DATA_SIZE = 4000;
|
||||
public static final int MAX_METADATA_LENGTH = 32;
|
||||
public static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
|
||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
public static final int MAX_IDENTIFIER_LENGTH = 64;
|
||||
|
||||
/** If time difference between transaction and now is greater than this then we don't verify proof-of-work. */
|
||||
public static final long HISTORIC_THRESHOLD = 2 * 7 * 24 * 60 * 60 * 1000L;
|
||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
|
||||
|
||||
// Constructors
|
||||
|
||||
public ArbitraryTransaction(Repository repository, TransactionData transactionData) {
|
||||
@@ -202,9 +207,11 @@ public class ArbitraryTransaction extends Transaction {
|
||||
// Clear nonce from transactionBytes
|
||||
ArbitraryTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
// Check nonce
|
||||
int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty();
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
// We only need to check nonce for recent transactions due to PoW verification overhead
|
||||
if (NTP.getTime() - this.arbitraryTransactionData.getTimestamp() < HISTORIC_THRESHOLD) {
|
||||
int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty();
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@@ -73,7 +73,7 @@ public class CancelGroupBanTransaction extends Transaction {
|
||||
Account member = getMember();
|
||||
|
||||
// Check ban actually exists
|
||||
if (!this.repository.getGroupRepository().banExists(groupId, member.getAddress()))
|
||||
if (!this.repository.getGroupRepository().banExists(groupId, member.getAddress(), this.groupUnbanTransactionData.getTimestamp()))
|
||||
return ValidationResult.BAN_UNKNOWN;
|
||||
|
||||
// Check admin has enough funds
|
||||
|
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.CancelSellNameTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -81,7 +82,13 @@ public class CancelSellNameTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData;
|
||||
|
||||
// Rebuild this name in the Names table from the transaction history
|
||||
// This is necessary because in some rare cases names can be missing from the Names table after registration
|
||||
// but we have been unable to reproduce the issue and track down the root cause
|
||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
namesDatabaseIntegrityCheck.rebuildName(cancelSellNameTransactionData.getName(), this.repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package org.qortal.transaction;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
@@ -16,9 +18,11 @@ import org.qortal.list.ResourceListManager;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.GroupRepository;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.ChatTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class ChatTransaction extends Transaction {
|
||||
|
||||
@@ -26,10 +30,11 @@ public class ChatTransaction extends Transaction {
|
||||
private ChatTransactionData chatTransactionData;
|
||||
|
||||
// Other useful constants
|
||||
public static final int MAX_DATA_SIZE = 1024;
|
||||
public static final int MAX_DATA_SIZE = 4000;
|
||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
public static final int POW_DIFFICULTY_WITH_QORT = 8; // leading zero bits
|
||||
public static final int POW_DIFFICULTY_NO_QORT = 12; // leading zero bits
|
||||
public static final int POW_DIFFICULTY_ABOVE_QORT_THRESHOLD = 8; // leading zero bits
|
||||
public static final int POW_DIFFICULTY_BELOW_QORT_THRESHOLD = 18; // leading zero bits
|
||||
public static final long POW_QORT_THRESHOLD = 400000000L;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -78,7 +83,7 @@ public class ChatTransaction extends Transaction {
|
||||
// Clear nonce from transactionBytes
|
||||
ChatTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
int difficulty = this.getSender().getConfirmedBalance(Asset.QORT) > 0 ? POW_DIFFICULTY_WITH_QORT : POW_DIFFICULTY_NO_QORT;
|
||||
int difficulty = this.getSender().getConfirmedBalance(Asset.QORT) >= POW_QORT_THRESHOLD ? POW_DIFFICULTY_ABOVE_QORT_THRESHOLD : POW_DIFFICULTY_BELOW_QORT_THRESHOLD;
|
||||
|
||||
// Calculate nonce
|
||||
this.chatTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, difficulty));
|
||||
@@ -145,6 +150,11 @@ public class ChatTransaction extends Transaction {
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
||||
|
||||
// Disregard messages with timestamp too far in the future (we have stricter limits for CHAT transactions)
|
||||
if (this.chatTransactionData.getTimestamp() > NTP.getTime() + (5 * 60 * 1000L)) {
|
||||
return ValidationResult.TIMESTAMP_TOO_NEW;
|
||||
}
|
||||
|
||||
// Check for blocked author by address
|
||||
ResourceListManager listManager = ResourceListManager.getInstance();
|
||||
if (listManager.listContains("blockedAddresses", this.chatTransactionData.getSender(), true)) {
|
||||
@@ -163,6 +173,14 @@ public class ChatTransaction extends Transaction {
|
||||
}
|
||||
}
|
||||
|
||||
PublicKeyAccount creator = this.getCreator();
|
||||
if (creator == null)
|
||||
return ValidationResult.MISSING_CREATOR;
|
||||
|
||||
// Reject if unconfirmed pile already has X recent CHAT transactions from same creator
|
||||
if (countRecentChatTransactionsByCreator(creator) >= Settings.getInstance().getMaxRecentChatMessagesPerAccount())
|
||||
return ValidationResult.TOO_MANY_UNCONFIRMED;
|
||||
|
||||
// If we exist in the repository then we've been imported as unconfirmed,
|
||||
// but we don't want to make it into a block, so return fake non-OK result.
|
||||
if (this.repository.getTransactionRepository().exists(this.chatTransactionData.getSignature()))
|
||||
@@ -204,7 +222,7 @@ public class ChatTransaction extends Transaction {
|
||||
|
||||
int difficulty;
|
||||
try {
|
||||
difficulty = this.getSender().getConfirmedBalance(Asset.QORT) > 0 ? POW_DIFFICULTY_WITH_QORT : POW_DIFFICULTY_NO_QORT;
|
||||
difficulty = this.getSender().getConfirmedBalance(Asset.QORT) >= POW_QORT_THRESHOLD ? POW_DIFFICULTY_ABOVE_QORT_THRESHOLD : POW_DIFFICULTY_BELOW_QORT_THRESHOLD;
|
||||
} catch (DataException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -213,6 +231,26 @@ public class ChatTransaction extends Transaction {
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
}
|
||||
|
||||
private int countRecentChatTransactionsByCreator(PublicKeyAccount creator) throws DataException {
|
||||
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
|
||||
final Long now = NTP.getTime();
|
||||
long recentThreshold = Settings.getInstance().getRecentChatMessagesMaxAge();
|
||||
|
||||
// We only care about chat transactions, and only those that are considered 'recent'
|
||||
Predicate<TransactionData> hasSameCreatorAndIsRecentChat = transactionData -> {
|
||||
if (transactionData.getType() != TransactionType.CHAT)
|
||||
return false;
|
||||
|
||||
if (transactionData.getTimestamp() < now - recentThreshold)
|
||||
return false;
|
||||
|
||||
return Arrays.equals(creator.getPublicKey(), transactionData.getCreatorPublicKey());
|
||||
};
|
||||
|
||||
return (int) unconfirmedTransactions.stream().filter(hasSameCreatorAndIsRecentChat).count();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure there's at least a skeleton account so people
|
||||
* can retrieve sender's public key using address, even if all their messages
|
||||
|
@@ -78,7 +78,7 @@ public class GroupInviteTransaction extends Transaction {
|
||||
return ValidationResult.ALREADY_GROUP_MEMBER;
|
||||
|
||||
// Check invitee is not banned
|
||||
if (this.repository.getGroupRepository().banExists(groupId, invitee.getAddress()))
|
||||
if (this.repository.getGroupRepository().banExists(groupId, invitee.getAddress(), this.groupInviteTransactionData.getTimestamp()))
|
||||
return ValidationResult.BANNED_FROM_GROUP;
|
||||
|
||||
// Check creator has enough funds
|
||||
|
@@ -53,7 +53,7 @@ public class JoinGroupTransaction extends Transaction {
|
||||
return ValidationResult.ALREADY_GROUP_MEMBER;
|
||||
|
||||
// Check member is not banned
|
||||
if (this.repository.getGroupRepository().banExists(groupId, joiner.getAddress()))
|
||||
if (this.repository.getGroupRepository().banExists(groupId, joiner.getAddress(), this.joinGroupTransactionData.getTimestamp()))
|
||||
return ValidationResult.BANNED_FROM_GROUP;
|
||||
|
||||
// Check join request doesn't already exist
|
||||
|
@@ -4,7 +4,9 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.data.transaction.PublicizeTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -26,7 +28,7 @@ public class PublicizeTransaction extends Transaction {
|
||||
/** If time difference between transaction and now is greater than this then we don't verify proof-of-work. */
|
||||
public static final long HISTORIC_THRESHOLD = 2 * 7 * 24 * 60 * 60 * 1000L;
|
||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
public static final int POW_DIFFICULTY = 15; // leading zero bits
|
||||
public static final int POW_DIFFICULTY = 14; // leading zero bits
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -102,6 +104,12 @@ public class PublicizeTransaction extends Transaction {
|
||||
if (!verifyNonce())
|
||||
return ValidationResult.INCORRECT_NONCE;
|
||||
|
||||
// Validate fee if one has been included
|
||||
PublicKeyAccount creator = this.getCreator();
|
||||
if (this.transactionData.getFee() > 0)
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.transactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
|
@@ -163,9 +163,12 @@ public class RewardShareTransaction extends Transaction {
|
||||
return ValidationResult.SELF_SHARE_EXISTS;
|
||||
}
|
||||
|
||||
// Fee checking needed if not setting up new self-share
|
||||
if (!(isRecipientAlsoMinter && existingRewardShareData == null))
|
||||
// Check creator has enough funds
|
||||
// Check creator has enough funds
|
||||
if (this.rewardShareTransactionData.getTimestamp() >= BlockChain.getInstance().getFeeValidationFixTimestamp())
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.rewardShareTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
else if (!(isRecipientAlsoMinter && existingRewardShareData == null))
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.rewardShareTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
|
@@ -245,6 +245,7 @@ public abstract class Transaction {
|
||||
ADDRESS_BLOCKED(96),
|
||||
NAME_BLOCKED(97),
|
||||
GROUP_APPROVAL_REQUIRED(98),
|
||||
ACCOUNT_NOT_TRANSFERABLE(99),
|
||||
INVALID_BUT_OK(999),
|
||||
NOT_YET_RELEASED(1000);
|
||||
|
||||
|
@@ -67,6 +67,11 @@ public class TransferPrivsTransaction extends Transaction {
|
||||
if (getSender().getConfirmedBalance(Asset.QORT) < this.transferPrivsTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
// Check sender doesn't have a blocksMintedPenalty, as these accounts cannot be transferred
|
||||
AccountData senderAccountData = this.repository.getAccountRepository().getAccount(getSender().getAddress());
|
||||
if (senderAccountData == null || senderAccountData.getBlocksMintedPenalty() != 0)
|
||||
return ValidationResult.ACCOUNT_NOT_TRANSFERABLE;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
|
@@ -103,7 +103,7 @@ public class UpdateGroupTransaction extends Transaction {
|
||||
Account newOwner = getNewOwner();
|
||||
|
||||
// Check new owner is not banned
|
||||
if (this.repository.getGroupRepository().banExists(this.updateGroupTransactionData.getGroupId(), newOwner.getAddress()))
|
||||
if (this.repository.getGroupRepository().banExists(this.updateGroupTransactionData.getGroupId(), newOwner.getAddress(), this.updateGroupTransactionData.getTimestamp()))
|
||||
return ValidationResult.BANNED_FROM_GROUP;
|
||||
|
||||
return ValidationResult.OK;
|
||||
|
@@ -24,6 +24,7 @@
|
||||
"onlineAccountSignaturesMinLifetime": 43200000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 1659801600000,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 5.00 },
|
||||
{ "height": 259201, "reward": 4.75 },
|
||||
@@ -81,8 +82,14 @@
|
||||
"transactionV6Timestamp": 9999999999999,
|
||||
"disableReferenceTimestamp": 1655222400000,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"chatReferenceTimestamp": 9999999999999
|
||||
"onlineAccountMinterLevelValidationHeight": 1092000,
|
||||
"selfSponsorshipAlgoV1Height": 1092400,
|
||||
"feeValidationFixTimestamp": 1671918000000,
|
||||
"chatReferenceTimestamp": 1674316800000
|
||||
},
|
||||
"checkpoints": [
|
||||
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }
|
||||
],
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
"timestamp": "1593450000000",
|
||||
|
83
src/main/resources/i18n/ApiError_pl.properties
Normal file
83
src/main/resources/i18n/ApiError_pl.properties
Normal 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
|
@@ -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 = нет ответа
|
||||
|
@@ -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
|
||||
|
46
src/main/resources/i18n/SysTray_pl.properties
Normal file
46
src/main/resources/i18n/SysTray_pl.properties
Normal 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
|
@@ -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 = Автоматическое обновление
|
||||
|
||||
|
195
src/main/resources/i18n/TransactionValidity_de.properties
Normal file
195
src/main/resources/i18n/TransactionValidity_de.properties
Normal 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
|
196
src/main/resources/i18n/TransactionValidity_pl.properties
Normal file
196
src/main/resources/i18n/TransactionValidity_pl.properties
Normal 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
|
||||
|
@@ -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...
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
1572
src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java
Normal file
1572
src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,7 @@ public class BlockApiTests extends ApiCommon {
|
||||
|
||||
@Test
|
||||
public void testGetBlockRange() {
|
||||
assertNotNull(this.blocksResource.getBlockRange(1, 1));
|
||||
assertNotNull(this.blocksResource.getBlockRange(1, 1, false, false));
|
||||
|
||||
List<Integer> testValues = Arrays.asList(null, Integer.valueOf(1));
|
||||
|
||||
|
@@ -1,11 +1,26 @@
|
||||
package org.qortal.test.arbitrary;
|
||||
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.arbitrary.misc.Service.ValidationResult;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.RegisterNameTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.ArbitraryUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -117,10 +132,27 @@ public class ArbitraryServiceTests extends Common {
|
||||
Service service = Service.GIF_REPOSITORY;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
// There is an index file in the root
|
||||
assertEquals(ValidationResult.OK, service.validate(path));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateSingleFileGifRepository() throws IOException {
|
||||
// Generate some random data
|
||||
byte[] data = new byte[1024];
|
||||
new Random().nextBytes(data);
|
||||
|
||||
// Write the data to a single file in a temp path
|
||||
Path path = Files.createTempDirectory("testValidateSingleFileGifRepository");
|
||||
path.toFile().deleteOnExit();
|
||||
Path imagePath = Paths.get(path.toString(), "image1.gif");
|
||||
Files.write(imagePath, data, StandardOpenOption.CREATE);
|
||||
|
||||
Service service = Service.GIF_REPOSITORY;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
assertEquals(ValidationResult.OK, service.validate(imagePath));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateMultiLayerGifRepository() throws IOException {
|
||||
// Generate some random data
|
||||
@@ -140,7 +172,6 @@ public class ArbitraryServiceTests extends Common {
|
||||
Service service = Service.GIF_REPOSITORY;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
// There is an index file in the root
|
||||
assertEquals(ValidationResult.DIRECTORIES_NOT_ALLOWED, service.validate(path));
|
||||
}
|
||||
|
||||
@@ -151,7 +182,6 @@ public class ArbitraryServiceTests extends Common {
|
||||
Service service = Service.GIF_REPOSITORY;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
// There is an index file in the root
|
||||
assertEquals(ValidationResult.MISSING_DATA, service.validate(path));
|
||||
}
|
||||
|
||||
@@ -171,8 +201,192 @@ public class ArbitraryServiceTests extends Common {
|
||||
Service service = Service.GIF_REPOSITORY;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
// There is an index file in the root
|
||||
assertEquals(ValidationResult.INVALID_FILE_EXTENSION, service.validate(path));
|
||||
}
|
||||
|
||||
}
|
||||
@Test
|
||||
public void testValidatePublishedGifRepository() throws IOException, DataException, MissingDataException, IllegalAccessException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Generate some random data
|
||||
byte[] data = new byte[1024];
|
||||
new Random().nextBytes(data);
|
||||
|
||||
// Write the data to several files in a temp path
|
||||
Path path = Files.createTempDirectory("testValidateGifRepository");
|
||||
path.toFile().deleteOnExit();
|
||||
Files.write(Paths.get(path.toString(), "image1.gif"), data, StandardOpenOption.CREATE);
|
||||
Files.write(Paths.get(path.toString(), "image2.gif"), data, StandardOpenOption.CREATE);
|
||||
Files.write(Paths.get(path.toString(), "image3.gif"), data, StandardOpenOption.CREATE);
|
||||
|
||||
Service service = Service.GIF_REPOSITORY;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
assertEquals(ValidationResult.OK, service.validate(path));
|
||||
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = "test_identifier";
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction
|
||||
ArbitraryUtils.createAndMintTxn(repository, publicKey58, path, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice);
|
||||
|
||||
// Build the latest data state for this name, and no exceptions should be thrown because validation passes
|
||||
ArbitraryDataReader arbitraryDataReader1a = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
arbitraryDataReader1a.loadSynchronously(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateQChatAttachment() throws IOException {
|
||||
// Generate some random data
|
||||
byte[] data = new byte[1024];
|
||||
new Random().nextBytes(data);
|
||||
|
||||
// Write the data a single file in a temp path
|
||||
Path path = Files.createTempDirectory("testValidateQChatAttachment");
|
||||
path.toFile().deleteOnExit();
|
||||
Files.write(Paths.get(path.toString(), "document.pdf"), data, StandardOpenOption.CREATE);
|
||||
|
||||
Service service = Service.QCHAT_ATTACHMENT;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
assertEquals(ValidationResult.OK, service.validate(path));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateSingleFileQChatAttachment() throws IOException {
|
||||
// Generate some random data
|
||||
byte[] data = new byte[1024];
|
||||
new Random().nextBytes(data);
|
||||
|
||||
// Write the data a single file in a temp path
|
||||
Path path = Files.createTempDirectory("testValidateSingleFileQChatAttachment");
|
||||
path.toFile().deleteOnExit();
|
||||
Path filePath = Paths.get(path.toString(), "document.pdf");
|
||||
Files.write(filePath, data, StandardOpenOption.CREATE);
|
||||
|
||||
Service service = Service.QCHAT_ATTACHMENT;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
assertEquals(ValidationResult.OK, service.validate(filePath));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateInvalidQChatAttachmentFileExtension() throws IOException {
|
||||
// Generate some random data
|
||||
byte[] data = new byte[1024];
|
||||
new Random().nextBytes(data);
|
||||
|
||||
// Write the data a single file in a temp path
|
||||
Path path = Files.createTempDirectory("testValidateInvalidQChatAttachmentFileExtension");
|
||||
path.toFile().deleteOnExit();
|
||||
Files.write(Paths.get(path.toString(), "application.exe"), data, StandardOpenOption.CREATE);
|
||||
|
||||
Service service = Service.QCHAT_ATTACHMENT;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
assertEquals(ValidationResult.INVALID_FILE_EXTENSION, service.validate(path));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateEmptyQChatAttachment() throws IOException {
|
||||
Path path = Files.createTempDirectory("testValidateEmptyQChatAttachment");
|
||||
|
||||
Service service = Service.QCHAT_ATTACHMENT;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
assertEquals(ValidationResult.INVALID_FILE_COUNT, service.validate(path));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateMultiLayerQChatAttachment() throws IOException {
|
||||
// Generate some random data
|
||||
byte[] data = new byte[1024];
|
||||
new Random().nextBytes(data);
|
||||
|
||||
// Write the data to several files in a temp path
|
||||
Path path = Files.createTempDirectory("testValidateMultiLayerQChatAttachment");
|
||||
path.toFile().deleteOnExit();
|
||||
Files.write(Paths.get(path.toString(), "file1.txt"), data, StandardOpenOption.CREATE);
|
||||
|
||||
Path subdirectory = Paths.get(path.toString(), "subdirectory");
|
||||
Files.createDirectories(subdirectory);
|
||||
Files.write(Paths.get(subdirectory.toString(), "file2.txt"), data, StandardOpenOption.CREATE);
|
||||
Files.write(Paths.get(subdirectory.toString(), "file3.txt"), data, StandardOpenOption.CREATE);
|
||||
|
||||
Service service = Service.QCHAT_ATTACHMENT;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
assertEquals(ValidationResult.DIRECTORIES_NOT_ALLOWED, service.validate(path));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateMultiFileQChatAttachment() throws IOException {
|
||||
// Generate some random data
|
||||
byte[] data = new byte[1024];
|
||||
new Random().nextBytes(data);
|
||||
|
||||
// Write the data to several files in a temp path
|
||||
Path path = Files.createTempDirectory("testValidateMultiFileQChatAttachment");
|
||||
path.toFile().deleteOnExit();
|
||||
Files.write(Paths.get(path.toString(), "file1.txt"), data, StandardOpenOption.CREATE);
|
||||
Files.write(Paths.get(path.toString(), "file2.txt"), data, StandardOpenOption.CREATE);
|
||||
|
||||
Service service = Service.QCHAT_ATTACHMENT;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
assertEquals(ValidationResult.INVALID_FILE_COUNT, service.validate(path));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidatePublishedQChatAttachment() throws IOException, DataException, MissingDataException, IllegalAccessException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Generate some random data
|
||||
byte[] data = new byte[1024];
|
||||
new Random().nextBytes(data);
|
||||
|
||||
// Write the data a single file in a temp path
|
||||
Path path = Files.createTempDirectory("testValidateSingleFileQChatAttachment");
|
||||
path.toFile().deleteOnExit();
|
||||
Path filePath = Paths.get(path.toString(), "document.pdf");
|
||||
Files.write(filePath, data, StandardOpenOption.CREATE);
|
||||
|
||||
Service service = Service.QCHAT_ATTACHMENT;
|
||||
assertTrue(service.isValidationRequired());
|
||||
|
||||
assertEquals(ValidationResult.OK, service.validate(filePath));
|
||||
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = "test_identifier";
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction
|
||||
ArbitraryUtils.createAndMintTxn(repository, publicKey58, filePath, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice);
|
||||
|
||||
// Build the latest data state for this name, and no exceptions should be thrown because validation passes
|
||||
ArbitraryDataReader arbitraryDataReader1a = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
arbitraryDataReader1a.loadSynchronously(true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user