mirror of
https://github.com/Qortal/qortal.git
synced 2025-11-02 15:27:03 +00:00
Compare commits
40 Commits
v4.2.3
...
batch-rewa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15eefa4177 | ||
|
|
8b69b65712 | ||
|
|
d453e80c6b | ||
|
|
4fb799ba38 | ||
|
|
29e56158ae | ||
|
|
f74c9672f6 | ||
|
|
eb6a834fd9 | ||
|
|
a08f10ece3 | ||
|
|
ad51073f25 | ||
|
|
5983e6ccc9 | ||
|
|
760788e82b | ||
|
|
d39131ffa9 | ||
|
|
3fbcc50503 | ||
|
|
26ac7e5be5 | ||
| b051f9be89 | |||
| 24ff3ab581 | |||
| 34382b6e69 | |||
|
|
086a9afa0e | ||
|
|
9428f9688f | ||
|
|
9f4a0b7957 | ||
|
|
3c8574a466 | ||
|
|
c428d7ce2e | ||
|
|
4feb8f46c8 | ||
|
|
1f7a60dfd8 | ||
|
|
379b850bbd | ||
|
|
7bb61ec564 | ||
|
|
b0224651c2 | ||
|
|
6bf2b99913 | ||
|
|
fd9d0c4e51 | ||
|
|
c2756a5872 | ||
|
|
ecfb9a7d6d | ||
|
|
dd9d3fdb22 | ||
|
|
fe840bbf02 | ||
|
|
e7901a391f | ||
|
|
18e880158f | ||
|
|
a3526d84bc | ||
|
|
ff7a87ab18 | ||
|
|
cc8cdcd93a | ||
|
|
ac433b1527 | ||
|
|
0693e26cda |
2
pom.xml
2
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>4.2.3</version>
|
||||
<version>4.3.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
|
||||
102
src/main/java/org/qortal/api/model/AtCreationRequest.java
Normal file
102
src/main/java/org/qortal/api/model/AtCreationRequest.java
Normal file
@@ -0,0 +1,102 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.bouncycastle.util.encoders.DecoderException;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class AtCreationRequest {
|
||||
|
||||
@Schema(description = "CIYAM AT version", example = "2")
|
||||
private short ciyamAtVersion;
|
||||
|
||||
@Schema(description = "base64-encoded code bytes", example = "")
|
||||
private String codeBytesBase64;
|
||||
|
||||
@Schema(description = "base64-encoded data bytes", example = "")
|
||||
private String dataBytesBase64;
|
||||
|
||||
private short numCallStackPages;
|
||||
private short numUserStackPages;
|
||||
private long minActivationAmount;
|
||||
|
||||
// Default constructor for JSON deserialization
|
||||
public AtCreationRequest() {}
|
||||
|
||||
// Getters and setters
|
||||
public short getCiyamAtVersion() {
|
||||
return ciyamAtVersion;
|
||||
}
|
||||
|
||||
public void setCiyamAtVersion(short ciyamAtVersion) {
|
||||
this.ciyamAtVersion = ciyamAtVersion;
|
||||
}
|
||||
|
||||
|
||||
public String getCodeBytesBase64() {
|
||||
return this.codeBytesBase64;
|
||||
}
|
||||
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
public byte[] getCodeBytes() {
|
||||
if (this.codeBytesBase64 != null) {
|
||||
try {
|
||||
return Base64.decode(this.codeBytesBase64);
|
||||
}
|
||||
catch (DecoderException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public String getDataBytesBase64() {
|
||||
return this.dataBytesBase64;
|
||||
}
|
||||
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
public byte[] getDataBytes() {
|
||||
if (this.dataBytesBase64 != null) {
|
||||
try {
|
||||
return Base64.decode(this.dataBytesBase64);
|
||||
}
|
||||
catch (DecoderException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public short getNumCallStackPages() {
|
||||
return numCallStackPages;
|
||||
}
|
||||
|
||||
public void setNumCallStackPages(short numCallStackPages) {
|
||||
this.numCallStackPages = numCallStackPages;
|
||||
}
|
||||
|
||||
public short getNumUserStackPages() {
|
||||
return numUserStackPages;
|
||||
}
|
||||
|
||||
public void setNumUserStackPages(short numUserStackPages) {
|
||||
this.numUserStackPages = numUserStackPages;
|
||||
}
|
||||
|
||||
public long getMinActivationAmount() {
|
||||
return minActivationAmount;
|
||||
}
|
||||
|
||||
public void setMinActivationAmount(long minActivationAmount) {
|
||||
this.minActivationAmount = minActivationAmount;
|
||||
}
|
||||
}
|
||||
@@ -724,7 +724,7 @@ public class ArbitraryResource {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
|
||||
try {
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, false);
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, true);
|
||||
if (transactionMetadata != null) {
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, true);
|
||||
if (resourceMetadata != null) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.api.model.AtCreationRequest;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -38,9 +39,14 @@ import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
@Path("/at")
|
||||
@Tag(name = "Automated Transactions")
|
||||
public class AtResource {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AtResource.class);
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
@@ -156,6 +162,52 @@ public class AtResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/create")
|
||||
@Operation(
|
||||
summary = "Create base58-encoded AT creation bytes from the provided parameters",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = AtCreationRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "AT creation bytes suitable for use in a DEPLOY_AT transaction",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String create(AtCreationRequest atCreationRequest) {
|
||||
if (atCreationRequest.getCiyamAtVersion() < 2) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "ciyamAtVersion must be at least 2");
|
||||
}
|
||||
if (atCreationRequest.getCodeBytes() == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Valid codeBytesBase64 must be supplied");
|
||||
}
|
||||
if (atCreationRequest.getDataBytes() == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Valid dataBytesBase64 must be supplied");
|
||||
}
|
||||
|
||||
byte[] creationBytes = MachineState.toCreationBytes(
|
||||
atCreationRequest.getCiyamAtVersion(),
|
||||
atCreationRequest.getCodeBytes(),
|
||||
atCreationRequest.getDataBytes(),
|
||||
atCreationRequest.getNumCallStackPages(),
|
||||
atCreationRequest.getNumUserStackPages(),
|
||||
atCreationRequest.getMinActivationAmount()
|
||||
);
|
||||
return Base58.encode(creationBytes);
|
||||
}
|
||||
@POST
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, DEPLOY_AT transaction",
|
||||
|
||||
@@ -127,6 +127,11 @@ public class ArbitraryDataRenderer {
|
||||
String filename = this.getFilename(unzippedPath, inPath);
|
||||
Path filePath = Paths.get(unzippedPath, filename);
|
||||
boolean usingCustomRouting = false;
|
||||
if (Files.isDirectory(filePath) && (!inPath.endsWith("/"))) {
|
||||
inPath = inPath + "/";
|
||||
filename = this.getFilename(unzippedPath, inPath);
|
||||
filePath = Paths.get(unzippedPath, filename);
|
||||
}
|
||||
|
||||
// If the file doesn't exist, we may need to route the request elsewhere, or cleanup
|
||||
if (!Files.exists(filePath)) {
|
||||
|
||||
@@ -84,6 +84,7 @@ public class Block {
|
||||
TRANSACTION_PROCESSING_FAILED(53),
|
||||
TRANSACTION_ALREADY_PROCESSED(54),
|
||||
TRANSACTION_NEEDS_APPROVAL(55),
|
||||
TRANSACTION_NOT_CONFIRMABLE(56),
|
||||
AT_STATES_MISMATCH(61),
|
||||
ONLINE_ACCOUNTS_INVALID(70),
|
||||
ONLINE_ACCOUNT_UNKNOWN(71),
|
||||
@@ -375,83 +376,107 @@ public class Block {
|
||||
|
||||
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);
|
||||
Long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
|
||||
byte[] encodedOnlineAccounts = new byte[0];
|
||||
int onlineAccountsCount = 0;
|
||||
byte[] onlineAccountsSignatures = null;
|
||||
|
||||
if (isBatchRewardDistributionBlock(height)) {
|
||||
// Batch reward distribution block - copy online accounts from recent block with highest online accounts count
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
int firstBlock = height - BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount();
|
||||
int lastBlock = height - 1;
|
||||
BlockData highOnlineAccountsBlock = repository.getBlockRepository().getBlockInRangeWithHighestOnlineAccountsCount(firstBlock, lastBlock);
|
||||
encodedOnlineAccounts = highOnlineAccountsBlock.getEncodedOnlineAccounts();
|
||||
onlineAccountsCount = highOnlineAccountsBlock.getOnlineAccountsCount();
|
||||
// No point in copying signatures since these aren't revalidated, and because of this onlineAccountsTimestamp must be null too
|
||||
onlineAccountsSignatures = null;
|
||||
onlineAccountsTimestamp = null;
|
||||
}
|
||||
else if (isOnlineAccountsBlock(height)) {
|
||||
// Standard online accounts block - add online accounts in regular way
|
||||
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
LOGGER.debug("No online accounts - not even our own?");
|
||||
return null;
|
||||
}
|
||||
// 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);
|
||||
|
||||
// Load sorted list of reward share public keys into memory, so that the indexes can be obtained.
|
||||
// This is up to 100x faster than querying each index separately. For 4150 reward share keys, it
|
||||
// was taking around 5000ms to query individually, vs 50ms using this approach.
|
||||
List<byte[]> allRewardSharePublicKeys = repository.getAccountRepository().getRewardSharePublicKeys();
|
||||
|
||||
// Map using index into sorted list of reward-shares as key
|
||||
Map<Integer, OnlineAccountData> indexedOnlineAccounts = new HashMap<>();
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys);
|
||||
if (accountIndex == null)
|
||||
// Online account (reward-share) with current timestamp but reward-share cancelled
|
||||
continue;
|
||||
|
||||
indexedOnlineAccounts.put(accountIndex, onlineAccountData);
|
||||
}
|
||||
List<Integer> accountIndexes = new ArrayList<>(indexedOnlineAccounts.keySet());
|
||||
accountIndexes.sort(null);
|
||||
|
||||
// Convert to compressed integer set
|
||||
ConciseSet onlineAccountsSet = new ConciseSet();
|
||||
onlineAccountsSet = onlineAccountsSet.convert(accountIndexes);
|
||||
byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
|
||||
int onlineAccountsCount = onlineAccountsSet.size();
|
||||
|
||||
// Collate all signatures
|
||||
Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
|
||||
.stream()
|
||||
.map(OnlineAccountData::getSignature)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Aggregated, single signature
|
||||
byte[] onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
|
||||
|
||||
// Add nonces to the end of the online accounts signatures
|
||||
try {
|
||||
// Create ordered list of nonce values
|
||||
List<Integer> nonces = new ArrayList<>();
|
||||
for (int i = 0; i < onlineAccountsCount; ++i) {
|
||||
Integer accountIndex = accountIndexes.get(i);
|
||||
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
|
||||
nonces.add(onlineAccountData.getNonce());
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Encode the nonces to a byte array
|
||||
byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces);
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
LOGGER.debug("No online accounts - not even our own?");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load sorted list of reward share public keys into memory, so that the indexes can be obtained.
|
||||
// This is up to 100x faster than querying each index separately. For 4150 reward share keys, it
|
||||
// was taking around 5000ms to query individually, vs 50ms using this approach.
|
||||
List<byte[]> allRewardSharePublicKeys = repository.getAccountRepository().getRewardSharePublicKeys();
|
||||
|
||||
// Map using index into sorted list of reward-shares as key
|
||||
Map<Integer, OnlineAccountData> indexedOnlineAccounts = new HashMap<>();
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys);
|
||||
if (accountIndex == null)
|
||||
// Online account (reward-share) with current timestamp but reward-share cancelled
|
||||
continue;
|
||||
|
||||
indexedOnlineAccounts.put(accountIndex, onlineAccountData);
|
||||
}
|
||||
List<Integer> accountIndexes = new ArrayList<>(indexedOnlineAccounts.keySet());
|
||||
accountIndexes.sort(null);
|
||||
|
||||
// Convert to compressed integer set
|
||||
ConciseSet onlineAccountsSet = new ConciseSet();
|
||||
onlineAccountsSet = onlineAccountsSet.convert(accountIndexes);
|
||||
encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
|
||||
onlineAccountsCount = onlineAccountsSet.size();
|
||||
|
||||
// Collate all signatures
|
||||
Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
|
||||
.stream()
|
||||
.map(OnlineAccountData::getSignature)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Aggregated, single signature
|
||||
onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
|
||||
|
||||
// Add nonces to the end of the online accounts signatures
|
||||
try {
|
||||
// Create ordered list of nonce values
|
||||
List<Integer> nonces = new ArrayList<>();
|
||||
for (int i = 0; i < onlineAccountsCount; ++i) {
|
||||
Integer accountIndex = accountIndexes.get(i);
|
||||
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
|
||||
nonces.add(onlineAccountData.getNonce());
|
||||
}
|
||||
|
||||
// Encode the nonces to a byte array
|
||||
byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces);
|
||||
|
||||
// Append the encoded nonces to the encoded online account signatures
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
outputStream.write(onlineAccountsSignatures);
|
||||
outputStream.write(encodedNonces);
|
||||
onlineAccountsSignatures = outputStream.toByteArray();
|
||||
} catch (TransformationException | IOException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Append the encoded nonces to the encoded online account signatures
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
outputStream.write(onlineAccountsSignatures);
|
||||
outputStream.write(encodedNonces);
|
||||
onlineAccountsSignatures = outputStream.toByteArray();
|
||||
}
|
||||
catch (TransformationException | IOException e) {
|
||||
return null;
|
||||
else {
|
||||
// No online accounts should be included in this block
|
||||
onlineAccountsTimestamp = null;
|
||||
}
|
||||
|
||||
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData,
|
||||
@@ -1063,6 +1088,40 @@ public class Block {
|
||||
if (accountIndexes.size() != this.blockData.getOnlineAccountsCount())
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
|
||||
// Online accounts should only be included in designated blocks; all others must be empty
|
||||
if (!this.isOnlineAccountsBlock()) {
|
||||
if (this.blockData.getOnlineAccountsCount() != 0 || accountIndexes.size() != 0) {
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
// Not a designated online accounts block and account count is 0. Everything is correct so no need to validate further.
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
// If this is a batch reward distribution block, ensure that online accounts have been copied from the correct previous block
|
||||
if (this.isBatchRewardDistributionBlock()) {
|
||||
int firstBlock = this.getBlockData().getHeight() - BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount();
|
||||
int lastBlock = this.getBlockData().getHeight() - 1;
|
||||
BlockData highOnlineAccountsBlock = repository.getBlockRepository().getBlockInRangeWithHighestOnlineAccountsCount(firstBlock, lastBlock);
|
||||
|
||||
if (this.blockData.getOnlineAccountsCount() != highOnlineAccountsBlock.getOnlineAccountsCount()) {
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
if (!Arrays.equals(this.blockData.getEncodedOnlineAccounts(), highOnlineAccountsBlock.getEncodedOnlineAccounts())) {
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
if (this.blockData.getOnlineAccountsSignatures() != null) {
|
||||
// Signatures are excluded to reduce block size
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
if (this.blockData.getOnlineAccountsTimestamp() != null) {
|
||||
// Online accounts timestamp must be null, because no signatures are included
|
||||
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||
}
|
||||
|
||||
// Online accounts have been correctly copied, and were already validated in earlier block, so consider them valid
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
List<RewardShareData> onlineRewardShares = repository.getAccountRepository().getRewardSharesByIndexes(accountIndexes.toArray());
|
||||
if (onlineRewardShares == null)
|
||||
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
|
||||
@@ -1251,6 +1310,13 @@ public class Block {
|
||||
|| transaction.getDeadline() <= this.blockData.getTimestamp())
|
||||
return ValidationResult.TRANSACTION_TIMESTAMP_INVALID;
|
||||
|
||||
// After feature trigger, check that this transaction is confirmable
|
||||
if (transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
|
||||
if (!transaction.isConfirmable()) {
|
||||
return ValidationResult.TRANSACTION_NOT_CONFIRMABLE;
|
||||
}
|
||||
}
|
||||
|
||||
// Check transaction isn't already included in a block
|
||||
if (this.repository.getTransactionRepository().isConfirmed(transactionData.getSignature()))
|
||||
return ValidationResult.TRANSACTION_ALREADY_PROCESSED;
|
||||
@@ -1475,11 +1541,15 @@ public class Block {
|
||||
LOGGER.trace(() -> String.format("Processing block %d", this.blockData.getHeight()));
|
||||
|
||||
if (this.blockData.getHeight() > 1) {
|
||||
// Increase account levels
|
||||
increaseAccountLevels();
|
||||
|
||||
// Distribute block rewards, including transaction fees, before transactions processed
|
||||
processBlockRewards();
|
||||
// Account levels and block rewards are only processed on block reward distribution blocks
|
||||
if (this.isRewardDistributionBlock()) {
|
||||
// Increase account levels
|
||||
increaseAccountLevels();
|
||||
|
||||
// Distribute block rewards, including transaction fees, before transactions processed
|
||||
processBlockRewards();
|
||||
}
|
||||
|
||||
if (this.blockData.getHeight() == 212937)
|
||||
// Apply fix for block 212937
|
||||
@@ -1539,9 +1609,13 @@ public class Block {
|
||||
}
|
||||
|
||||
// Increase blocks minted count for all accounts
|
||||
int delta = 1;
|
||||
if (this.isBatchRewardDistributionActive()) {
|
||||
delta = BlockChain.getInstance().getBlockRewardBatchSize();
|
||||
}
|
||||
|
||||
// Batch update in repository
|
||||
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1);
|
||||
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +delta);
|
||||
|
||||
// Keep track of level bumps in case we need to apply to other entries
|
||||
Map<String, Integer> bumpedAccounts = new HashMap<>();
|
||||
@@ -1591,8 +1665,32 @@ public class Block {
|
||||
protected void processBlockRewards() throws DataException {
|
||||
// General block reward
|
||||
long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight());
|
||||
// Add transaction fees
|
||||
|
||||
if (this.isBatchRewardDistributionActive()) {
|
||||
// Batch distribution is active - so multiply the reward by the batch size
|
||||
reward *= BlockChain.getInstance().getBlockRewardBatchSize();
|
||||
|
||||
if (!this.isRewardDistributionBlock()) {
|
||||
// Shouldn't ever happen, but checking here for safety
|
||||
throw new DataException("Attempted to distribute a batch reward in a non-reward-distribution block");
|
||||
}
|
||||
|
||||
// Add transaction fees since last distribution block
|
||||
int firstBlock = this.getBlockData().getHeight() - BlockChain.getInstance().getBlockRewardBatchSize() + 1;
|
||||
int lastBlock = this.blockData.getHeight() - 1;
|
||||
Long totalFees = repository.getBlockRepository().getTotalFeesInBlockRange(firstBlock, lastBlock);
|
||||
if (totalFees == null) {
|
||||
throw new DataException("Unable to calculate total fees for block range");
|
||||
}
|
||||
reward += totalFees;
|
||||
LOGGER.debug("Total fees for range {} - {} when processing: {}", firstBlock, lastBlock, totalFees);
|
||||
}
|
||||
|
||||
// Add transaction fees for this block (it was excluded from the range above as it's not in the repository yet)
|
||||
reward += this.blockData.getTotalFees();
|
||||
LOGGER.debug("Total fees when processing block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees());
|
||||
|
||||
LOGGER.debug("Block reward when processing block {}: {}", this.blockData.getHeight(), reward);
|
||||
|
||||
// Nothing to reward?
|
||||
if (reward <= 0)
|
||||
@@ -1750,11 +1848,14 @@ public class Block {
|
||||
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
|
||||
SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
|
||||
|
||||
// Block rewards, including transaction fees, removed after transactions undone
|
||||
orphanBlockRewards();
|
||||
// Account levels and block rewards are only processed/orphaned on block reward distribution blocks
|
||||
if (this.isRewardDistributionBlock()) {
|
||||
// Block rewards, including transaction fees, removed after transactions undone
|
||||
orphanBlockRewards();
|
||||
|
||||
// Decrease account levels
|
||||
decreaseAccountLevels();
|
||||
// Decrease account levels
|
||||
decreaseAccountLevels();
|
||||
}
|
||||
}
|
||||
|
||||
// Delete block from blockchain
|
||||
@@ -1830,8 +1931,32 @@ public class Block {
|
||||
protected void orphanBlockRewards() throws DataException {
|
||||
// General block reward
|
||||
long reward = BlockChain.getInstance().getRewardAtHeight(this.blockData.getHeight());
|
||||
// Add transaction fees
|
||||
|
||||
if (this.isBatchRewardDistributionActive()) {
|
||||
// Batch distribution is active - so multiply the reward by the batch size
|
||||
reward *= BlockChain.getInstance().getBlockRewardBatchSize();
|
||||
|
||||
if (!this.isRewardDistributionBlock()) {
|
||||
// Shouldn't ever happen, but checking here for safety
|
||||
throw new DataException("Attempted to orphan batched rewards in a non-reward-distribution block");
|
||||
}
|
||||
|
||||
// Add transaction fees since last distribution block
|
||||
int firstBlock = this.getBlockData().getHeight() - BlockChain.getInstance().getBlockRewardBatchSize() + 1;
|
||||
int lastBlock = this.blockData.getHeight() - 1;
|
||||
Long totalFees = repository.getBlockRepository().getTotalFeesInBlockRange(firstBlock, lastBlock);
|
||||
if (totalFees == null) {
|
||||
throw new DataException("Unable to calculate total fees for block range");
|
||||
}
|
||||
reward += totalFees;
|
||||
LOGGER.debug("Total fees for range {} - {} when orphaning: {}", firstBlock, lastBlock, totalFees);
|
||||
}
|
||||
|
||||
// Add transaction fees for this block (it was excluded from the range above as it's not in the repository yet)
|
||||
reward += this.blockData.getTotalFees();
|
||||
LOGGER.debug("Total fees when orphaning block {}: {}", this.blockData.getHeight(), this.blockData.getTotalFees());
|
||||
|
||||
LOGGER.debug("Block reward when orphaning block {}: {}", this.blockData.getHeight(), reward);
|
||||
|
||||
// Nothing to reward?
|
||||
if (reward <= 0)
|
||||
@@ -1872,9 +1997,13 @@ public class Block {
|
||||
}
|
||||
|
||||
// Decrease blocks minted count for all accounts
|
||||
int delta = 1;
|
||||
if (this.isBatchRewardDistributionActive()) {
|
||||
delta = BlockChain.getInstance().getBlockRewardBatchSize();
|
||||
}
|
||||
|
||||
// Batch update in repository
|
||||
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), -1);
|
||||
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), -delta);
|
||||
|
||||
for (AccountData accountData : allUniqueExpandedAccounts) {
|
||||
// Adjust count locally (in Java)
|
||||
@@ -1897,6 +2026,105 @@ public class Block {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specifies whether the batch reward feature trigger has activated yet.
|
||||
* Note that the exact block of the feature trigger activation will return false,
|
||||
* because this is actually the very last block with non-batched reward distributions.
|
||||
*
|
||||
* @return true if active, false if batch rewards feature trigger height not reached yet.
|
||||
*/
|
||||
public boolean isBatchRewardDistributionActive() {
|
||||
return Block.isBatchRewardDistributionActive(this.blockData.getHeight());
|
||||
}
|
||||
public static boolean isBatchRewardDistributionActive(int height) {
|
||||
// Once the getBlockRewardBatchStartHeight is reached, reward distributions per block must stop.
|
||||
// Note the > instead of >= below, as the first batch distribution isn't until 1000 blocks *after* the
|
||||
// start height. The block exactly matching the start height is not batched.
|
||||
return height > BlockChain.getInstance().getBlockRewardBatchStartHeight();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specifies whether rewards are distributed in this block, via ANY method (batch or single).
|
||||
*
|
||||
* @return true if rewards are to be distributed in this block.
|
||||
*/
|
||||
public boolean isRewardDistributionBlock() {
|
||||
return Block.isRewardDistributionBlock(this.blockData.getHeight());
|
||||
}
|
||||
|
||||
public static boolean isRewardDistributionBlock(int height) {
|
||||
// Up to and *including* the start height (feature trigger), the rewards are distributed in every block
|
||||
if (!Block.isBatchRewardDistributionActive(height)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// After the start height (feature trigger) the rewards are distributed in blocks that are multiples of the batch size
|
||||
return height % BlockChain.getInstance().getBlockRewardBatchSize() == 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specifies whether BATCH rewards are distributed in this block.
|
||||
*
|
||||
* @return true if a batch distribution will occur, false if a single no distribution will occur.
|
||||
*/
|
||||
public boolean isBatchRewardDistributionBlock() {
|
||||
return Block.isBatchRewardDistributionBlock(this.blockData.getHeight());
|
||||
}
|
||||
|
||||
public static boolean isBatchRewardDistributionBlock(int height) {
|
||||
// Up to and *including* the start height (feature trigger), batch reward distribution isn't active yet
|
||||
if (!Block.isBatchRewardDistributionActive(height)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// After the start height (feature trigger) the rewards are distributed in blocks that are multiples of the batch size
|
||||
return height % BlockChain.getInstance().getBlockRewardBatchSize() == 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specifies whether online accounts are to be included in this block.
|
||||
*
|
||||
* @return true if online accounts should be included, false if they should be excluded.
|
||||
*/
|
||||
public boolean isOnlineAccountsBlock() {
|
||||
return Block.isOnlineAccountsBlock(this.getBlockData().getHeight());
|
||||
}
|
||||
|
||||
private static boolean isOnlineAccountsBlock(int height) {
|
||||
// After feature trigger, only certain blocks contain online accounts
|
||||
if (height >= BlockChain.getInstance().getBlockRewardBatchStartHeight()) {
|
||||
final int leadingBlockCount = BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount();
|
||||
return height >= (getNextBatchDistributionBlockHeight(height) - leadingBlockCount);
|
||||
}
|
||||
// Before feature trigger, all blocks contain online accounts
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param currentHeight
|
||||
*
|
||||
* @return the next height of a batch reward distribution. Must only be called after the
|
||||
* batch reward feature trigger has activated. It is not useful prior to this.
|
||||
*/
|
||||
private static int getNextBatchDistributionBlockHeight(int currentHeight) {
|
||||
final int batchSize = BlockChain.getInstance().getBlockRewardBatchSize();
|
||||
if (currentHeight % batchSize == 0) {
|
||||
// Already a reward distribution block
|
||||
return currentHeight;
|
||||
} else {
|
||||
// Calculate the difference needed to reach the next distribution block
|
||||
final int difference = batchSize - (currentHeight % batchSize);
|
||||
return currentHeight + difference;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class BlockRewardCandidate {
|
||||
public final String description;
|
||||
public long share;
|
||||
|
||||
@@ -209,6 +209,22 @@ public class BlockChain {
|
||||
/** Snapshot timestamp for self sponsorship algo V1 */
|
||||
private long selfSponsorshipAlgoV1SnapshotTimestamp;
|
||||
|
||||
/** Feature-trigger timestamp to modify behaviour of various transactions that support mempow */
|
||||
private long mempowTransactionUpdatesTimestamp;
|
||||
|
||||
/** Feature trigger block height for batch block reward payouts.
|
||||
* This MUST be a multiple of blockRewardBatchSize. Can't use
|
||||
* featureTriggers because unit tests need to set this value via Reflection. */
|
||||
private int blockRewardBatchStartHeight;
|
||||
|
||||
/** Block reward batch size. Must be (significantly) less than block prune size,
|
||||
* as all blocks in the range need to be present in the repository when processing/orphaning */
|
||||
private int blockRewardBatchSize;
|
||||
|
||||
/** Number of blocks prior to the batch reward distribution blocks to include online accounts
|
||||
* data and to base online accounts decisions on. */
|
||||
private int blockRewardBatchAccountsBlockCount;
|
||||
|
||||
/** Max reward shares by block height */
|
||||
public static class MaxRewardSharesByTimestamp {
|
||||
public long timestamp;
|
||||
@@ -365,11 +381,31 @@ public class BlockChain {
|
||||
return this.onlineAccountsModulusV2Timestamp;
|
||||
}
|
||||
|
||||
|
||||
/* Block reward batching */
|
||||
public long getBlockRewardBatchStartHeight() {
|
||||
return this.blockRewardBatchStartHeight;
|
||||
}
|
||||
|
||||
public int getBlockRewardBatchSize() {
|
||||
return this.blockRewardBatchSize;
|
||||
}
|
||||
|
||||
public int getBlockRewardBatchAccountsBlockCount() {
|
||||
return this.blockRewardBatchAccountsBlockCount;
|
||||
}
|
||||
|
||||
|
||||
// Self sponsorship algo
|
||||
public long getSelfSponsorshipAlgoV1SnapshotTimestamp() {
|
||||
return this.selfSponsorshipAlgoV1SnapshotTimestamp;
|
||||
}
|
||||
|
||||
// Feature-trigger timestamp to modify behaviour of various transactions that support mempow
|
||||
public long getMemPoWTransactionUpdatesTimestamp() {
|
||||
return this.mempowTransactionUpdatesTimestamp;
|
||||
}
|
||||
|
||||
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
|
||||
public boolean getRequireGroupForApproval() {
|
||||
return this.requireGroupForApproval;
|
||||
@@ -645,6 +681,22 @@ public class BlockChain {
|
||||
|
||||
if (totalShareV2 < 0 || totalShareV2 > 1_00000000L)
|
||||
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
|
||||
|
||||
// Check that blockRewardBatchSize isn't zero
|
||||
if (this.blockRewardBatchSize <= 0)
|
||||
Settings.throwValidationError("\"blockRewardBatchSize\" must be greater than 0");
|
||||
|
||||
// Check that blockRewardBatchStartHeight is a multiple of blockRewardBatchSize
|
||||
if (this.blockRewardBatchStartHeight % this.blockRewardBatchSize != 0)
|
||||
Settings.throwValidationError("\"blockRewardBatchStartHeight\" must be a multiple of \"blockRewardBatchSize\"");
|
||||
|
||||
// Check that blockRewardBatchAccountsBlockCount isn't zero
|
||||
if (this.blockRewardBatchAccountsBlockCount <= 0)
|
||||
Settings.throwValidationError("\"blockRewardBatchAccountsBlockCount\" must be greater than 0");
|
||||
|
||||
// Check that blockRewardBatchSize isn't zero
|
||||
if (this.blockRewardBatchAccountsBlockCount > this.blockRewardBatchSize)
|
||||
Settings.throwValidationError("\"blockRewardBatchAccountsBlockCount\" must be less than or equal to \"blockRewardBatchSize\"");
|
||||
}
|
||||
|
||||
/** Minor normalization, cached value generation, etc. */
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.block.CommonBlockData;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
@@ -36,6 +37,7 @@ import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
// Minting new blocks
|
||||
|
||||
@@ -126,10 +128,6 @@ public class BlockMinter extends Thread {
|
||||
if (minLatestBlockTimestamp == null)
|
||||
continue;
|
||||
|
||||
// No online accounts for current timestamp? (e.g. during startup)
|
||||
if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
|
||||
continue;
|
||||
|
||||
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
|
||||
// No minting accounts?
|
||||
if (mintingAccountsData.isEmpty())
|
||||
@@ -545,6 +543,18 @@ public class BlockMinter extends Thread {
|
||||
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
||||
}
|
||||
|
||||
public static Block mintTestingBlockUnvalidatedWithoutOnlineAccounts(Repository repository, PrivateKeyAccount mintingAccount) throws DataException {
|
||||
if (!BlockChain.getInstance().isTestChain())
|
||||
throw new DataException("Ignoring attempt to mint testing block for non-test chain!");
|
||||
|
||||
// Make sure there are no online accounts
|
||||
OnlineAccountsManager.getInstance().removeAllOnlineAccounts();
|
||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
|
||||
assertTrue(onlineAccounts.isEmpty());
|
||||
|
||||
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
||||
}
|
||||
|
||||
public static Block mintTestingBlockRetainingTimestamps(Repository repository, PrivateKeyAccount mintingAccount) throws DataException {
|
||||
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
|
||||
@@ -778,6 +778,13 @@ public class OnlineAccountsManager {
|
||||
}
|
||||
|
||||
|
||||
// Utils
|
||||
|
||||
public void removeAllOnlineAccounts() {
|
||||
this.currentOnlineAccounts.clear();
|
||||
}
|
||||
|
||||
|
||||
// Network handlers
|
||||
|
||||
public void onNetworkGetOnlineAccountsV3Message(Peer peer, Message message) {
|
||||
|
||||
@@ -218,12 +218,6 @@ public class TransactionImporter extends Thread {
|
||||
LOGGER.debug("Finished validating signatures in incoming transactions queue (valid this round: {}, total pending import: {})...", validatedCount, sigValidTransactions.size());
|
||||
}
|
||||
|
||||
if (!newlyValidSignatures.isEmpty()) {
|
||||
LOGGER.debug("Broadcasting {} newly valid signatures ahead of import", newlyValidSignatures.size());
|
||||
Message newTransactionSignatureMessage = new TransactionSignaturesMessage(newlyValidSignatures);
|
||||
Network.getInstance().broadcast(broadcastPeer -> newTransactionSignatureMessage);
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue while processing incoming transactions", e);
|
||||
}
|
||||
@@ -263,6 +257,9 @@ public class TransactionImporter extends Thread {
|
||||
unconfirmedTransactions.removeIf(t -> t.getType() == Transaction.TransactionType.CHAT);
|
||||
unconfirmedTransactionsCache = unconfirmedTransactions;
|
||||
|
||||
// A list of signatures were imported in this round
|
||||
List<byte[]> newlyImportedSignatures = new ArrayList<>();
|
||||
|
||||
// Import transactions with valid signatures
|
||||
try {
|
||||
for (int i = 0; i < sigValidTransactions.size(); ++i) {
|
||||
@@ -295,6 +292,15 @@ public class TransactionImporter extends Thread {
|
||||
|
||||
case OK: {
|
||||
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
|
||||
|
||||
// Add to the unconfirmed transactions cache
|
||||
if (transactionData.getType() != Transaction.TransactionType.CHAT && unconfirmedTransactionsCache != null) {
|
||||
unconfirmedTransactionsCache.add(transactionData);
|
||||
}
|
||||
|
||||
// Signature imported in this round
|
||||
newlyImportedSignatures.add(transactionData.getSignature());
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -323,6 +329,12 @@ public class TransactionImporter extends Thread {
|
||||
// Transaction has been processed, even if only to reject it
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
}
|
||||
|
||||
if (!newlyImportedSignatures.isEmpty()) {
|
||||
LOGGER.debug("Broadcasting {} newly imported signatures", newlyImportedSignatures.size());
|
||||
Message newTransactionSignatureMessage = new TransactionSignaturesMessage(newlyImportedSignatures);
|
||||
Network.getInstance().broadcast(broadcastPeer -> newTransactionSignatureMessage);
|
||||
}
|
||||
} finally {
|
||||
LOGGER.debug("Finished importing {} incoming transaction{}", processedCount, (processedCount == 1 ? "" : "s"));
|
||||
blockchainLock.unlock();
|
||||
|
||||
@@ -275,7 +275,10 @@ public class ArbitraryDataManager extends Thread {
|
||||
int offset = 0;
|
||||
|
||||
while (!isStopping) {
|
||||
Thread.sleep(1000L);
|
||||
final int minSeconds = 3;
|
||||
final int maxSeconds = 10;
|
||||
final int randomSleepTime = new Random().nextInt((maxSeconds - minSeconds + 1)) + minSeconds;
|
||||
Thread.sleep(randomSleepTime * 1000L);
|
||||
|
||||
// Any arbitrary transactions we want to fetch data for?
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
@@ -317,20 +318,36 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
// Do this in a new thread so caller doesn't have to wait for computeNonce()
|
||||
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
|
||||
new Thread(() -> {
|
||||
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
// reset repository state to prevent deadlock
|
||||
threadsRepository.discardChanges();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
|
||||
}
|
||||
}, "TradeBot response").start();
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
||||
@@ -548,15 +565,25 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
outgoingMessageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
|
||||
outgoingMessageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (outgoingMessageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -668,15 +695,25 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// Reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
@@ -317,20 +318,36 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
// Do this in a new thread so caller doesn't have to wait for computeNonce()
|
||||
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
|
||||
new Thread(() -> {
|
||||
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
// reset repository state to prevent deadlock
|
||||
threadsRepository.discardChanges();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
|
||||
}
|
||||
}, "TradeBot response").start();
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
||||
@@ -548,15 +565,25 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
outgoingMessageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
|
||||
outgoingMessageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (outgoingMessageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -668,15 +695,25 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// Reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
@@ -317,20 +318,36 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
// Do this in a new thread so caller doesn't have to wait for computeNonce()
|
||||
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
|
||||
new Thread(() -> {
|
||||
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
// reset repository state to prevent deadlock
|
||||
threadsRepository.discardChanges();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
|
||||
}
|
||||
}, "TradeBot response").start();
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
||||
@@ -548,15 +565,25 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
outgoingMessageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
|
||||
outgoingMessageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (outgoingMessageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -668,15 +695,25 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// Reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
@@ -317,20 +318,36 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
// Do this in a new thread so caller doesn't have to wait for computeNonce()
|
||||
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
|
||||
new Thread(() -> {
|
||||
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
// reset repository state to prevent deadlock
|
||||
threadsRepository.discardChanges();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
|
||||
}
|
||||
}, "TradeBot response").start();
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
||||
@@ -548,15 +565,25 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
outgoingMessageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
|
||||
outgoingMessageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (outgoingMessageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -668,15 +695,25 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// Reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,15 +325,24 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
threadsRepository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
|
||||
@@ -556,15 +565,25 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
outgoingMessageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
|
||||
outgoingMessageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (outgoingMessageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -676,15 +695,25 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// Reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
@@ -320,20 +321,36 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
// Do this in a new thread so caller doesn't have to wait for computeNonce()
|
||||
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
|
||||
new Thread(() -> {
|
||||
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
// reset repository state to prevent deadlock
|
||||
threadsRepository.discardChanges();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
|
||||
}
|
||||
}, "TradeBot response").start();
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddressT3));
|
||||
@@ -561,15 +578,25 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
outgoingMessageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
|
||||
outgoingMessageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (outgoingMessageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -681,15 +708,25 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// Reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
@@ -317,20 +318,36 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
// Do this in a new thread so caller doesn't have to wait for computeNonce()
|
||||
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
|
||||
new Thread(() -> {
|
||||
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
// reset repository state to prevent deadlock
|
||||
threadsRepository.discardChanges();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
|
||||
}
|
||||
}, "TradeBot response").start();
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
||||
@@ -548,15 +565,25 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
outgoingMessageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
|
||||
outgoingMessageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (outgoingMessageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -668,15 +695,25 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
|
||||
messageTransaction.computeNonce();
|
||||
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
|
||||
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// Reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
if (messageTransaction.isSignatureValid()) {
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ public class TradeBot implements Listener {
|
||||
SysTray.getInstance().showMessage("Trade-Bot", String.format("%s: %s", tradeBotData.getAtAddress(), newState), MessageType.INFO);
|
||||
|
||||
if (logMessageSupplier != null)
|
||||
LOGGER.info(logMessageSupplier);
|
||||
LOGGER.info(logMessageSupplier.get());
|
||||
|
||||
LOGGER.debug(() -> String.format("new state for trade-bot entry based on AT %s: %s", tradeBotData.getAtAddress(), newState));
|
||||
|
||||
|
||||
@@ -191,10 +191,18 @@ public class BlockData implements Serializable {
|
||||
return this.encodedOnlineAccounts;
|
||||
}
|
||||
|
||||
public void setEncodedOnlineAccounts(byte[] encodedOnlineAccounts) {
|
||||
this.encodedOnlineAccounts = encodedOnlineAccounts;
|
||||
}
|
||||
|
||||
public int getOnlineAccountsCount() {
|
||||
return this.onlineAccountsCount;
|
||||
}
|
||||
|
||||
public void setOnlineAccountsCount(int onlineAccountsCount) {
|
||||
this.onlineAccountsCount = onlineAccountsCount;
|
||||
}
|
||||
|
||||
public Long getOnlineAccountsTimestamp() {
|
||||
return this.onlineAccountsTimestamp;
|
||||
}
|
||||
|
||||
@@ -132,6 +132,17 @@ public interface BlockRepository {
|
||||
*/
|
||||
public List<BlockData> getBlocks(int firstBlockHeight, int lastBlockHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns blocks within height range.
|
||||
*/
|
||||
public Long getTotalFeesInBlockRange(int firstBlockHeight, int lastBlockHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns block with highest online accounts count in specified range. If more than one block
|
||||
* has the same high count, the oldest one is returned.
|
||||
*/
|
||||
public BlockData getBlockInRangeWithHighestOnlineAccountsCount(int firstBlockHeight, int lastBlockHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns block summaries for the passed height range.
|
||||
*/
|
||||
|
||||
@@ -357,6 +357,36 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTotalFeesInBlockRange(int firstBlockHeight, int lastBlockHeight) throws DataException {
|
||||
String sql = "SELECT SUM(total_fees) AS sum_total_fees FROM Blocks WHERE height BETWEEN ? AND ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, firstBlockHeight, lastBlockHeight)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
long totalFees = resultSet.getLong(1);
|
||||
|
||||
return totalFees;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Error fetching total fees in block range from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData getBlockInRangeWithHighestOnlineAccountsCount(int firstBlockHeight, int lastBlockHeight) throws DataException {
|
||||
String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height BETWEEN ? AND ? "
|
||||
+ "ORDER BY online_accounts_count DESC, height ASC "
|
||||
+ "LIMIT 1";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, firstBlockHeight, lastBlockHeight)) {
|
||||
return getBlockFromResultSet(resultSet);
|
||||
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Error fetching highest online accounts block in range from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException {
|
||||
String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count, reference "
|
||||
|
||||
@@ -147,7 +147,7 @@ public class Settings {
|
||||
private int blockCacheSize = 10;
|
||||
|
||||
/** Maximum number of transactions for the block minter to include in a block */
|
||||
private int maxTransactionsPerBlock = 25;
|
||||
private int maxTransactionsPerBlock = 50;
|
||||
|
||||
/** How long to keep old, full, AT state data (ms). */
|
||||
private long atStatesMaxLifetime = 5 * 24 * 60 * 60 * 1000L; // milliseconds
|
||||
@@ -230,7 +230,7 @@ public class Settings {
|
||||
public long recoveryModeTimeout = 9999999999999L;
|
||||
|
||||
/** Minimum peer version number required in order to sync with them */
|
||||
private String minPeerVersion = "4.1.2";
|
||||
private String minPeerVersion = "4.3.0";
|
||||
/** 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 */
|
||||
@@ -941,7 +941,9 @@ public class Settings {
|
||||
}
|
||||
|
||||
public int getPruneBlockLimit() {
|
||||
return this.pruneBlockLimit;
|
||||
// Never prune more than twice the block reward batch size, as the data is needed when processing/orphaning
|
||||
int minPruneBlockLimit = BlockChain.getInstance().getBlockRewardBatchSize() * 2;
|
||||
return Math.max(this.pruneBlockLimit, minPruneBlockLimit);
|
||||
}
|
||||
|
||||
public long getAtStatesPruneInterval() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.qortal.transaction;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -8,7 +7,6 @@ import java.util.stream.Collectors;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
@@ -88,8 +86,14 @@ public class ArbitraryTransaction extends Transaction {
|
||||
if (this.transactionData.getFee() < 0)
|
||||
return ValidationResult.NEGATIVE_FEE;
|
||||
|
||||
// After the feature trigger, we require the fee to be sufficient if it's not 0.
|
||||
// If the fee is zero, then the nonce is validated in isSignatureValid() as an alternative to a fee
|
||||
// As of the mempow transaction updates timestamp, a nonce is no longer supported, so a valid fee must be included
|
||||
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
|
||||
// Validate the fee
|
||||
return super.isFeeValid();
|
||||
}
|
||||
|
||||
// After the earlier "optional fee" feature trigger, we required the fee to be sufficient if it wasn't 0.
|
||||
// If the fee was zero, then the nonce was validated in isSignatureValid() as an alternative to a fee
|
||||
if (this.arbitraryTransactionData.getTimestamp() >= BlockChain.getInstance().getArbitraryOptionalFeeTimestamp() && this.arbitraryTransactionData.getFee() != 0L) {
|
||||
return super.isFeeValid();
|
||||
}
|
||||
@@ -214,7 +218,13 @@ public class ArbitraryTransaction extends Transaction {
|
||||
// Clear nonce from transactionBytes
|
||||
ArbitraryTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
// As of feature-trigger timestamp, we only require a nonce when the fee is zero
|
||||
// As of the mempow transaction updates timestamp, a nonce is no longer supported, so a fee must be included
|
||||
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
|
||||
// Require that the fee is a positive number. Fee checking itself is performed in isFeeValid()
|
||||
return (this.arbitraryTransactionData.getFee() > 0L);
|
||||
}
|
||||
|
||||
// As of the earlier "optional fee" feature-trigger timestamp, we only required a nonce when the fee was zero
|
||||
boolean beforeFeatureTrigger = this.arbitraryTransactionData.getTimestamp() < BlockChain.getInstance().getArbitraryOptionalFeeTimestamp();
|
||||
if (beforeFeatureTrigger || this.arbitraryTransactionData.getFee() == 0L) {
|
||||
// We only need to check nonce for recent transactions due to PoW verification overhead
|
||||
|
||||
@@ -148,6 +148,12 @@ public class ChatTransaction extends Transaction {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfirmable() {
|
||||
// CHAT transactions can't go into blocks
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
||||
|
||||
@@ -33,7 +33,9 @@ public class MessageTransaction extends Transaction {
|
||||
|
||||
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 = 14; // leading zero bits
|
||||
public static final int POW_DIFFICULTY_V1 = 14; // leading zero bits
|
||||
public static final int POW_DIFFICULTY_V2_CONFIRMABLE = 16; // leading zero bits
|
||||
public static final int POW_DIFFICULTY_V2_UNCONFIRMABLE = 12; // leading zero bits
|
||||
|
||||
// Properties
|
||||
|
||||
@@ -109,7 +111,17 @@ public class MessageTransaction extends Transaction {
|
||||
MessageTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
// Calculate nonce
|
||||
this.messageTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY));
|
||||
this.messageTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, getPoWDifficulty()));
|
||||
}
|
||||
|
||||
public int getPoWDifficulty() {
|
||||
// The difficulty changes at the "mempow transactions updates" timestamp
|
||||
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
|
||||
// If this message is confirmable then require a higher difficulty
|
||||
return this.isConfirmable() ? POW_DIFFICULTY_V2_CONFIRMABLE : POW_DIFFICULTY_V2_UNCONFIRMABLE;
|
||||
}
|
||||
// Before feature trigger timestamp, so use existing difficulty value
|
||||
return POW_DIFFICULTY_V1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,6 +195,18 @@ public class MessageTransaction extends Transaction {
|
||||
return super.hasValidReference();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfirmable() {
|
||||
// After feature trigger timestamp, only messages to an AT address can confirm
|
||||
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
|
||||
if (this.messageTransactionData.getRecipient() == null || !this.messageTransactionData.getRecipient().toUpperCase().startsWith("A")) {
|
||||
// Message isn't to an AT address, so this transaction is unconfirmable
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
||||
@@ -235,7 +259,7 @@ public class MessageTransaction extends Transaction {
|
||||
MessageTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
// Check nonce
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce);
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, getPoWDifficulty(), nonce);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -256,6 +280,11 @@ public class MessageTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Only certain MESSAGE transactions are able to confirm
|
||||
if (!this.isConfirmable()) {
|
||||
throw new DataException("Unconfirmable MESSAGE transactions should never be processed");
|
||||
}
|
||||
|
||||
// If we have no amount then there's nothing to do
|
||||
if (this.messageTransactionData.getAmount() == 0L)
|
||||
return;
|
||||
@@ -280,6 +309,11 @@ public class MessageTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public void orphan() throws DataException {
|
||||
// Only certain MESSAGE transactions are able to confirm
|
||||
if (!this.isConfirmable()) {
|
||||
throw new DataException("Unconfirmable MESSAGE transactions should never be orphaned");
|
||||
}
|
||||
|
||||
// If we have no amount then there's nothing to do
|
||||
if (this.messageTransactionData.getAmount() == 0L)
|
||||
return;
|
||||
|
||||
@@ -155,6 +155,12 @@ public class PresenceTransaction extends Transaction {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfirmable() {
|
||||
// PRESENCE transactions can't go into blocks
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.block.BlockChain;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.data.transaction.PublicizeTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -89,6 +90,12 @@ public class PublicizeTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Disable completely after feature-trigger timestamp, at the same time that mempow difficulties are being increased.
|
||||
// It could be enabled again in the future, but preferably with an enforced minimum fee instead of allowing a mempow nonce.
|
||||
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
|
||||
return ValidationResult.NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// There can be only one
|
||||
List<byte[]> signatures = this.repository.getTransactionRepository().getSignaturesMatchingCriteria(
|
||||
TransactionType.PUBLICIZE,
|
||||
|
||||
@@ -248,7 +248,8 @@ public abstract class Transaction {
|
||||
GROUP_APPROVAL_REQUIRED(98),
|
||||
ACCOUNT_NOT_TRANSFERABLE(99),
|
||||
INVALID_BUT_OK(999),
|
||||
NOT_YET_RELEASED(1000);
|
||||
NOT_YET_RELEASED(1000),
|
||||
NOT_SUPPORTED(1001);
|
||||
|
||||
public final int value;
|
||||
|
||||
@@ -636,7 +637,7 @@ public abstract class Transaction {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sorted, unconfirmed transactions, excluding invalid.
|
||||
* Returns sorted, unconfirmed transactions, excluding invalid and unconfirmable.
|
||||
*
|
||||
* @return sorted, unconfirmed transactions
|
||||
* @throws DataException
|
||||
@@ -654,7 +655,8 @@ public abstract class Transaction {
|
||||
TransactionData transactionData = unconfirmedTransactionsIterator.next();
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
if (transaction.isStillValidUnconfirmed(latestBlockData.getTimestamp()) != ValidationResult.OK)
|
||||
// Must be confirmable and valid
|
||||
if (!transaction.isConfirmable() || transaction.isStillValidUnconfirmed(latestBlockData.getTimestamp()) != ValidationResult.OK)
|
||||
unconfirmedTransactionsIterator.remove();
|
||||
}
|
||||
|
||||
@@ -892,6 +894,17 @@ public abstract class Transaction {
|
||||
/* To be optionally overridden */
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether transaction is 'confirmable' - i.e. is of a type that
|
||||
* can be included in a block. Some transactions are 'unconfirmable'
|
||||
* and therefore must remain in the mempool until they expire.
|
||||
* @return
|
||||
*/
|
||||
public boolean isConfirmable() {
|
||||
/* To be optionally overridden */
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether transaction can be added to the blockchain.
|
||||
* <p>
|
||||
|
||||
@@ -460,6 +460,10 @@ public class BlockTransformer extends Transformer {
|
||||
}
|
||||
|
||||
public static ConciseSet decodeOnlineAccounts(byte[] encodedOnlineAccounts) {
|
||||
if (encodedOnlineAccounts.length == 0) {
|
||||
return new ConciseSet();
|
||||
}
|
||||
|
||||
int[] words = new int[encodedOnlineAccounts.length / 4];
|
||||
ByteBuffer.wrap(encodedOnlineAccounts).asIntBuffer().get(words);
|
||||
return new ConciseSet(words, false);
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 1659801600000,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000,
|
||||
"mempowTransactionUpdatesTimestamp": 1693558800000,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 1000,
|
||||
"blockRewardBatchAccountsBlockCount": 25,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 5.00 },
|
||||
{ "height": 259201, "reward": 4.75 },
|
||||
|
||||
@@ -4,52 +4,52 @@
|
||||
# "localeLang": "de",
|
||||
|
||||
### Common ###
|
||||
JSON = JSON Nachricht konnte nicht geparst werden
|
||||
JSON = JSON-Nachricht konnte nicht geparst werden
|
||||
|
||||
INSUFFICIENT_BALANCE = Kein Ausgleich
|
||||
INSUFFICIENT_BALANCE = Guthaben reicht nicht aus
|
||||
|
||||
UNAUTHORIZED = API-Aufruf nicht autorisiert
|
||||
|
||||
REPOSITORY_ISSUE = Repository-Fehler
|
||||
|
||||
NON_PRODUCTION = Dieser APi-Aufruf ist nicht gestattet für Produtkion
|
||||
NON_PRODUCTION = dieser API-Aufruf ist für Produktionssysteme nicht gestattet
|
||||
|
||||
BLOCKCHAIN_NEEDS_SYNC = Blockchain muss sich erst verbinden
|
||||
BLOCKCHAIN_NEEDS_SYNC = Blockchain muss sich erst synchronisieren
|
||||
|
||||
NO_TIME_SYNC = noch keine Uhrensynchronisation
|
||||
NO_TIME_SYNC = Uhrzeit noch nicht synchronisiert
|
||||
|
||||
### Validation ###
|
||||
INVALID_SIGNATURE = ungültige Signatur
|
||||
INVALID_SIGNATURE = Signatur ungültig
|
||||
|
||||
INVALID_ADDRESS = ungültige Adresse
|
||||
INVALID_ADDRESS = Adresse ungültig
|
||||
|
||||
INVALID_PUBLIC_KEY = ungültiger public key
|
||||
INVALID_PUBLIC_KEY = öffentlicher Schlüssel ungültig
|
||||
|
||||
INVALID_DATA = ungültige Daten
|
||||
INVALID_DATA = Daten ungültig
|
||||
|
||||
INVALID_NETWORK_ADDRESS = ungültige Netzwerk Adresse
|
||||
INVALID_NETWORK_ADDRESS = Netzwerk Adresse ungültig
|
||||
|
||||
ADDRESS_UNKNOWN = Account Adresse unbekannt
|
||||
ADDRESS_UNKNOWN = Kontoadresse unbekannt
|
||||
|
||||
INVALID_CRITERIA = ungültige Suchkriterien
|
||||
INVALID_CRITERIA = Suchkriterien ungültig
|
||||
|
||||
INVALID_REFERENCE = ungültige Referenz
|
||||
INVALID_REFERENCE = Referenz ungültig
|
||||
|
||||
TRANSFORMATION_ERROR = konnte JSON nicht in eine Transaktion umwandeln
|
||||
|
||||
INVALID_PRIVATE_KEY = ungültiger private key
|
||||
INVALID_PRIVATE_KEY = öffentlicher Schlüssel ungültig
|
||||
|
||||
INVALID_HEIGHT = ungültige block height
|
||||
INVALID_HEIGHT = Blockhöhe ungültig
|
||||
|
||||
CANNOT_MINT = Account kann nicht minten
|
||||
CANNOT_MINT = Konto kann nicht prägen
|
||||
|
||||
### Blocks ###
|
||||
BLOCK_UNKNOWN = block unbekannt
|
||||
BLOCK_UNKNOWN = Block unbekannt
|
||||
|
||||
### Transactions ###
|
||||
TRANSACTION_UNKNOWN = Transaktion unbekannt
|
||||
|
||||
PUBLIC_KEY_NOT_FOUND = public key wurde nicht gefunden
|
||||
PUBLIC_KEY_NOT_FOUND = öffentlicher Schlüssel wurde nicht gefunden
|
||||
|
||||
# this one is special in that caller expected to pass two additional strings, hence the two %s
|
||||
TRANSACTION_INVALID = Transaktion ungültig: %s (%s)
|
||||
@@ -58,19 +58,19 @@ TRANSACTION_INVALID = Transaktion ungültig: %s (%s)
|
||||
NAME_UNKNOWN = Name unbekannt
|
||||
|
||||
### Asset ###
|
||||
INVALID_ASSET_ID = ungültige asset ID
|
||||
INVALID_ASSET_ID = Vermögenswert-Kennung ungültig
|
||||
|
||||
INVALID_ORDER_ID = ungültige asset order ID
|
||||
INVALID_ORDER_ID = Vermögenswert-Auftragssnummer ungültig
|
||||
|
||||
ORDER_UNKNOWN = unbekannte asset order ID
|
||||
ORDER_UNKNOWN = Vermögenswert-Auftragssnummer unbekannt
|
||||
|
||||
### Groups ###
|
||||
GROUP_UNKNOWN = Gruppe unbekannt
|
||||
|
||||
### Foreign Blockchain ###
|
||||
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = fremde Blockchain oder ElectrumX Netzwerk Problem
|
||||
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = fremde Blockchain oder ElectrumX Netzwerkproblem
|
||||
|
||||
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = unzureichend Bilanz auf fremde blockchain
|
||||
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = unzureichendes Guthaben auf fremder blockchain
|
||||
|
||||
FOREIGN_BLOCKCHAIN_TOO_SOON = zu früh um fremde Blockchain-Transaktionen zu übertragen (Sperrzeit/mittlere Blockzeit)
|
||||
|
||||
@@ -80,4 +80,4 @@ ORDER_SIZE_TOO_SMALL = Bestellmenge zu niedrig
|
||||
### Data ###
|
||||
FILE_NOT_FOUND = Datei nicht gefunden
|
||||
|
||||
NO_REPLY = Peer hat nicht mit Daten verbinden
|
||||
NO_REPLY = Peer hat nicht in vorgegebener Zeit geantwortet
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# SysTray pop-up menu
|
||||
|
||||
APPLYING_UPDATE_AND_RESTARTING = Automatisches Update anwenden und neu starten …
|
||||
APPLYING_UPDATE_AND_RESTARTING = Automatisches Update anwenden und neu starten...
|
||||
|
||||
AUTO_UPDATE = Automatisches Update
|
||||
|
||||
@@ -19,7 +19,7 @@ CONNECTION = Verbindung
|
||||
|
||||
CONNECTIONS = Verbindungen
|
||||
|
||||
CREATING_BACKUP_OF_DB_FILES = Erstellen Backup von Datenbank Dateien …
|
||||
CREATING_BACKUP_OF_DB_FILES = Erstelle Backup von Datenbank Dateien...
|
||||
|
||||
DB_BACKUP = Datenbank Backup
|
||||
|
||||
@@ -31,18 +31,18 @@ EXIT = Verlassen
|
||||
|
||||
LITE_NODE = Lite node
|
||||
|
||||
MINTING_DISABLED = Kein minting
|
||||
MINTING_DISABLED = Münzprägung inaktiv
|
||||
|
||||
MINTING_ENABLED = \u2714 Minting aktiviert
|
||||
MINTING_ENABLED = \u2714 Münzprägung aktiv
|
||||
|
||||
OPEN_UI = Öffne UI
|
||||
OPEN_UI = Öffne Benutzeroberfläche
|
||||
|
||||
PERFORMING_DB_CHECKPOINT = Speichern von unbestätigten Datenbankänderungen...
|
||||
PERFORMING_DB_CHECKPOINT = Speichere unerfasste Datenbankänderungen...
|
||||
|
||||
PERFORMING_DB_MAINTENANCE = Planmäßige Wartung durchführen...
|
||||
PERFORMING_DB_MAINTENANCE = Planmäßige Wartung wird durchgeführt...
|
||||
|
||||
SYNCHRONIZE_CLOCK = Synchronisiere Uhr
|
||||
SYNCHRONIZE_CLOCK = Synchronisiere Uhrzeit
|
||||
|
||||
SYNCHRONIZING_BLOCKCHAIN = Synchronisierung der Blockchain
|
||||
SYNCHRONIZING_BLOCKCHAIN = Synchronisiere
|
||||
|
||||
SYNCHRONIZING_CLOCK = Synchronisierung der Uhr
|
||||
SYNCHRONIZING_CLOCK = Uhrzeit wird synchronisiert
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
#
|
||||
|
||||
ACCOUNT_ALREADY_EXISTS = Account existiert bereits
|
||||
ACCOUNT_ALREADY_EXISTS = Konto existiert bereits
|
||||
|
||||
ACCOUNT_CANNOT_REWARD_SHARE = Account kann keine Belohnung teilen
|
||||
ACCOUNT_CANNOT_REWARD_SHARE = Konto kann nicht an Belohnungsbeteiligung teilnehmen
|
||||
|
||||
ADDRESS_ABOVE_RATE_LIMIT = address hat das angegebene Geschwindigkeitlimit erreicht
|
||||
ADDRESS_ABOVE_RATE_LIMIT = Adresse hat festgelegtes Tarif-Limit erreicht
|
||||
|
||||
ADDRESS_BLOCKED = Addresse ist geblockt
|
||||
ADDRESS_BLOCKED = diese Adresse ist gesperrt
|
||||
|
||||
ALREADY_GROUP_ADMIN = bereits Gruppen Admin
|
||||
ALREADY_GROUP_ADMIN = bereits Gruppenadmin
|
||||
|
||||
ALREADY_GROUP_MEMBER = bereits Gruppen Mitglied
|
||||
ALREADY_GROUP_MEMBER = bereits Gruppenmitglied
|
||||
|
||||
ALREADY_VOTED_FOR_THAT_OPTION = bereits für diese Option gestimmt
|
||||
|
||||
ASSET_ALREADY_EXISTS = asset existiert bereits
|
||||
ASSET_ALREADY_EXISTS = Vermögenswert existiert bereits
|
||||
|
||||
ASSET_DOES_NOT_EXIST = asset nicht gefunden
|
||||
ASSET_DOES_NOT_EXIST = Vermögenswert existiert nicht
|
||||
|
||||
ASSET_DOES_NOT_MATCH_AT = asset passt nicht mit AT's asset
|
||||
ASSET_DOES_NOT_MATCH_AT = Vermögenswert stimmt nicht mit dem Vermögenswert von AT überein
|
||||
|
||||
ASSET_NOT_SPENDABLE = asset ist nicht ausgabefähig
|
||||
ASSET_NOT_SPENDABLE = Vermögenswert ist nicht auszahlbar
|
||||
|
||||
AT_ALREADY_EXISTS = AT existiert bereits
|
||||
|
||||
AT_IS_FINISHED = AT ist fertig
|
||||
AT_IS_FINISHED = AT ist beendet
|
||||
|
||||
AT_UNKNOWN = AT unbekannt
|
||||
AT_UNKNOWN = AT unbekannt
|
||||
|
||||
BAN_EXISTS = ban besteht bereits
|
||||
BAN_EXISTS = Bann ist bereits vorhanden
|
||||
|
||||
BAN_UNKNOWN = ban unbekannt
|
||||
BAN_UNKNOWN = Bann unbekannt
|
||||
|
||||
BANNED_FROM_GROUP = von der gruppe gebannt
|
||||
BANNED_FROM_GROUP = aus der Gruppe verbannt
|
||||
|
||||
BUYER_ALREADY_OWNER = Käufer ist bereits Besitzer
|
||||
BUYER_ALREADY_OWNER = Käufer ist bereits Eigentümer
|
||||
|
||||
CLOCK_NOT_SYNCED = Uhr nicht synchronisiert
|
||||
CLOCK_NOT_SYNCED = Uhrzeit ist nicht synchronisiert
|
||||
|
||||
DUPLICATE_MESSAGE = Adresse sendete doppelte Nachricht
|
||||
DUPLICATE_MESSAGE = Adresse hat doppelte Nachricht gesendet
|
||||
|
||||
DUPLICATE_OPTION = Duplizierungsmöglichkeit
|
||||
DUPLICATE_OPTION = doppelte Option
|
||||
|
||||
GROUP_ALREADY_EXISTS = Gruppe besteht bereits
|
||||
GROUP_ALREADY_EXISTS = Gruppe existiert bereits
|
||||
|
||||
GROUP_APPROVAL_DECIDED = Gruppenfreigabe bereits beschlossen
|
||||
GROUP_APPROVAL_DECIDED = Gruppenzulassung bereits beschlossen
|
||||
|
||||
GROUP_APPROVAL_NOT_REQUIRED = Gruppenfreigabe nicht erforderlich
|
||||
GROUP_APPROVAL_NOT_REQUIRED = Gruppenzustimmung nicht erforderlich
|
||||
|
||||
GROUP_DOES_NOT_EXIST = Gruppe nicht vorhanden
|
||||
GROUP_DOES_NOT_EXIST = Gruppe existiert nicht
|
||||
|
||||
GROUP_ID_MISMATCH = Gruppen-ID stimmt nicht überein
|
||||
|
||||
GROUP_OWNER_CANNOT_LEAVE = Gruppenbesitzer kann Gruppe nicht verlassen
|
||||
GROUP_OWNER_CANNOT_LEAVE = Gruppeneigentümer kann Gruppe nicht verlassen
|
||||
|
||||
HAVE_EQUALS_WANT = das bessesene-asset ist das selbe wie das gesuchte-asset
|
||||
HAVE_EQUALS_WANT = Haben-Vermögenswert ist derselbe wie Wollen-Vermögenswert
|
||||
|
||||
INCORRECT_NONCE = falsche PoW-Nonce
|
||||
|
||||
@@ -64,81 +64,81 @@ INVALID_ADDRESS = ungültige Adresse
|
||||
|
||||
INVALID_AMOUNT = ungültiger Betrag
|
||||
|
||||
INVALID_ASSET_OWNER = Ungültiger Eigentümer
|
||||
INVALID_ASSET_OWNER = ungültiger Vermögenswert-Eigentümer
|
||||
|
||||
INVALID_AT_TRANSACTION = ungültige AT-Transaktion
|
||||
|
||||
INVALID_AT_TYPE_LENGTH = ungültige AT 'Typ' Länge
|
||||
INVALID_AT_TYPE_LENGTH = ungültige AT-Typ-Länge
|
||||
|
||||
INVALID_BUT_OK = ungültig aber OK
|
||||
INVALID_BUT_OK = ungültig, aber OK
|
||||
|
||||
INVALID_CREATION_BYTES = ungültige Erstellungs der bytes
|
||||
INVALID_CREATION_BYTES = ungültige Erstellungsbytes
|
||||
|
||||
INVALID_DATA_LENGTH = ungültige Datenlänge
|
||||
INVALID_DATA_LENGTH = unzulässige Datenlänge
|
||||
|
||||
INVALID_DESCRIPTION_LENGTH = ungültige Länge der Beschreibung
|
||||
INVALID_DESCRIPTION_LENGTH = unzulässige 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_BLOCK_DELAY = ungültige Blockverzögerung bei der Gruppenfreigabe
|
||||
|
||||
INVALID_GROUP_ID = ungültige Gruppen-ID
|
||||
|
||||
INVALID_GROUP_OWNER = ungültiger Gruppenbesitzer
|
||||
INVALID_GROUP_OWNER = ungültiger Gruppeneigentümer
|
||||
|
||||
INVALID_LIFETIME = unzulässige Lebensdauer
|
||||
INVALID_LIFETIME = unzulässige Gültigkeitsdauer
|
||||
|
||||
INVALID_NAME_LENGTH = ungültige Namenslänge
|
||||
|
||||
INVALID_NAME_OWNER = ungültiger Besitzername
|
||||
INVALID_NAME_OWNER = ungültiger Eigentümer des Namens
|
||||
|
||||
INVALID_OPTION_LENGTH = ungültige Länge der Optionen
|
||||
|
||||
INVALID_OPTIONS_COUNT = Anzahl ungültiger Optionen
|
||||
INVALID_OPTIONS_COUNT = ungültige Anzahl von Optionen
|
||||
|
||||
INVALID_ORDER_CREATOR = ungültiger Auftragsersteller
|
||||
|
||||
INVALID_PAYMENTS_COUNT = Anzahl ungültiger Zahlungen
|
||||
INVALID_PAYMENTS_COUNT = ungültige Anzahl der Zahlungen
|
||||
|
||||
INVALID_PUBLIC_KEY = ungültiger öffentlicher Schlüssel
|
||||
|
||||
INVALID_QUANTITY = unzulässige Menge
|
||||
INVALID_QUANTITY = ungültige Menge
|
||||
|
||||
INVALID_REFERENCE = ungültige Referenz
|
||||
|
||||
INVALID_RETURN = ungültige Rückgabe
|
||||
|
||||
INVALID_REWARD_SHARE_PERCENT = ungültig Prozent der Belohnunganteile
|
||||
INVALID_REWARD_SHARE_PERCENT = ungültiger Belohnungsbeteiligungs-Anteil
|
||||
|
||||
INVALID_SELLER = unzulässiger Verkäufer
|
||||
INVALID_SELLER = ungültiger Verkäufer
|
||||
|
||||
INVALID_TAGS_LENGTH = ungültige 'tags'-Länge
|
||||
|
||||
INVALID_TIMESTAMP_SIGNATURE = Ungültige Zeitstempel-Signatur
|
||||
INVALID_TIMESTAMP_SIGNATURE = ungültige Zeitstempel-Signatur
|
||||
|
||||
INVALID_TX_GROUP_ID = Ungültige Transaktionsgruppen-ID
|
||||
INVALID_TX_GROUP_ID = ungültige Transaktionsgruppen-ID
|
||||
|
||||
INVALID_VALUE_LENGTH = ungültige 'Wert'-Länge
|
||||
INVALID_VALUE_LENGTH = ungültige 'value'-Länge
|
||||
|
||||
INVITE_UNKNOWN = Gruppeneinladung unbekannt
|
||||
|
||||
JOIN_REQUEST_EXISTS = Gruppeneinladung existiert bereits
|
||||
JOIN_REQUEST_EXISTS = Gruppenverbindungsanfrage existiert bereits
|
||||
|
||||
MAXIMUM_REWARD_SHARES = die maximale Anzahl von Reward-Shares für dieses Konto erreicht
|
||||
MAXIMUM_REWARD_SHARES = maximale Anzahl von Belohnungsbeteiligungen für dieses Konto bereits erreicht
|
||||
|
||||
MISSING_CREATOR = fehlender Ersteller
|
||||
|
||||
MULTIPLE_NAMES_FORBIDDEN = mehrere registrierte Namen pro Konto sind untersagt
|
||||
MULTIPLE_NAMES_FORBIDDEN = mehrere registrierte Namen pro Konto sind verboten
|
||||
|
||||
NAME_ALREADY_FOR_SALE = Name bereits zum Verkauf
|
||||
NAME_ALREADY_FOR_SALE = Name steht bereits zum Verkauf
|
||||
|
||||
NAME_ALREADY_REGISTERED = Name bereits registriert
|
||||
|
||||
NAME_BLOCKED = Name geblockt
|
||||
NAME_BLOCKED = dieser Name ist gesperrt
|
||||
|
||||
NAME_DOES_NOT_EXIST = Name nicht vorhanden
|
||||
NAME_DOES_NOT_EXIST = Name existiert nicht
|
||||
|
||||
NAME_NOT_FOR_SALE = Name ist unverkäuflich
|
||||
NAME_NOT_FOR_SALE = Name steht nicht zum Verkauf
|
||||
|
||||
NAME_NOT_NORMALIZED = Name nicht in Unicode-'normalisierter' Form
|
||||
|
||||
@@ -150,46 +150,46 @@ NEGATIVE_PRICE = ungültiger/negativer Preis
|
||||
|
||||
NO_BALANCE = unzureichendes Guthaben
|
||||
|
||||
NO_BLOCKCHAIN_LOCK = die Blockchain des Knotens ist beschäftigt
|
||||
NO_BLOCKCHAIN_LOCK = die Blockchain des Knotens ist derzeit beschäftigt
|
||||
|
||||
NO_FLAG_PERMISSION = Konto hat diese Berechtigung nicht
|
||||
|
||||
NOT_GROUP_ADMIN = Account ist kein Gruppenadmin
|
||||
NOT_GROUP_ADMIN = Konto ist kein Gruppenadmin
|
||||
|
||||
NOT_GROUP_MEMBER = Account kein Gruppenmitglied
|
||||
NOT_GROUP_MEMBER = Konto ist kein Gruppenmitglied
|
||||
|
||||
NOT_MINTING_ACCOUNT = Account kann nicht minten
|
||||
NOT_MINTING_ACCOUNT = Konto kann nicht prägen
|
||||
|
||||
NOT_YET_RELEASED = Funktion noch nicht freigegeben
|
||||
|
||||
OK = OK
|
||||
|
||||
ORDER_ALREADY_CLOSED = Asset Trade Order ist bereits geschlossen
|
||||
ORDER_ALREADY_CLOSED = Vermögenswert-Handelsauftrag ist bereits geschlossen
|
||||
|
||||
ORDER_DOES_NOT_EXIST = asset trade order existiert nicht
|
||||
ORDER_DOES_NOT_EXIST = Vermögenswert-Handelsauftrag existiert nicht
|
||||
|
||||
POLL_ALREADY_EXISTS = Umfrage bereits vorhanden
|
||||
POLL_ALREADY_EXISTS = Umfrage existiert bereits
|
||||
|
||||
POLL_DOES_NOT_EXIST = Umfrage nicht vorhanden
|
||||
POLL_DOES_NOT_EXIST = Umfrage existiert nicht
|
||||
|
||||
POLL_OPTION_DOES_NOT_EXIST = Umfrageoption existiert nicht
|
||||
POLL_OPTION_DOES_NOT_EXIST = Umfrageoption nicht vorhanden
|
||||
|
||||
PUBLIC_KEY_UNKNOWN = öffentlicher Schlüssel unbekannt
|
||||
|
||||
REWARD_SHARE_UNKNOWN = Geteilte Belohnungen unbekant
|
||||
REWARD_SHARE_UNKNOWN = Belohnungsbeteiligung unbekannt
|
||||
|
||||
SELF_SHARE_EXISTS = Selbstbeteiligung (Geteilte Belohnungen) sind breits vorhanden
|
||||
SELF_SHARE_EXISTS = Selbst-Beteiligung (Belohnungsbeteiligung) existiert bereits
|
||||
|
||||
TIMESTAMP_TOO_NEW = Zeitstempel zu neu
|
||||
|
||||
TIMESTAMP_TOO_OLD = Zeitstempel zu alt
|
||||
|
||||
TOO_MANY_UNCONFIRMED = Account hat zu viele unbestätigte Transaktionen am laufen
|
||||
TOO_MANY_UNCONFIRMED = Konto hat zu viele ausstehende unbestätigte Transaktionen
|
||||
|
||||
TRANSACTION_ALREADY_CONFIRMED = Transaktionen sind bereits bestätigt
|
||||
TRANSACTION_ALREADY_CONFIRMED = Transaktion wurde bereits bestätigt
|
||||
|
||||
TRANSACTION_ALREADY_EXISTS = Transaktionen existiert bereits
|
||||
TRANSACTION_ALREADY_EXISTS = Transaktion existiert bereits
|
||||
|
||||
TRANSACTION_UNKNOWN = Unbekante Transaktion
|
||||
TRANSACTION_UNKNOWN = Transaktion unbekannt
|
||||
|
||||
TX_GROUP_ID_MISMATCH = Transaktion Gruppen ID stimmt nicht überein
|
||||
TX_GROUP_ID_MISMATCH = die Gruppen-ID der Transaktion stimmt nicht überein
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.qortal.test;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@@ -39,13 +41,14 @@ public class MemoryPoWTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleComputes() {
|
||||
public void testMultipleComputes() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
Random random = new Random();
|
||||
|
||||
final int sampleSize = 20;
|
||||
final int sampleSize = 10;
|
||||
final long stddevDivisor = sampleSize * (sampleSize - 1);
|
||||
|
||||
for (int difficulty = 8; difficulty < 16; difficulty += 2) {
|
||||
for (int difficulty = 8; difficulty <= 16; difficulty++) {
|
||||
byte[] data = new byte[256];
|
||||
long[] times = new long[sampleSize];
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.qortal.test;
|
||||
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -14,12 +15,9 @@ import org.qortal.group.Group.ApprovalThreshold;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.GroupUtils;
|
||||
import org.qortal.test.common.TestAccount;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.*;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
@@ -31,6 +29,7 @@ import org.qortal.utils.NTP;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class MessageTests extends Common {
|
||||
@@ -139,7 +138,7 @@ public class MessageTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withRecipentWithAmount() throws DataException {
|
||||
public void withRecipientWithAmount() throws DataException {
|
||||
testMessage(Group.NO_GROUP, recipient, 123L, Asset.QORT);
|
||||
}
|
||||
|
||||
@@ -153,6 +152,140 @@ public class MessageTests extends Common {
|
||||
testMessage(1, null, 0L, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atRecipientNoFeeWithNonce() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String atRecipient = deployAt();
|
||||
MessageTransaction transaction = testFeeNonce(repository, false, true, atRecipient, true);
|
||||
|
||||
// Transaction should be confirmable because it's to an AT, and therefore should be present in a block
|
||||
assertTrue(transaction.isConfirmable());
|
||||
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
|
||||
assertTrue(isTransactionConfirmed(repository, transaction));
|
||||
assertEquals(16, transaction.getPoWDifficulty());
|
||||
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regularRecipientNoFeeWithNonce() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
|
||||
// Transaction should not be present in db yet
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, recipient, null, null, null);
|
||||
assertTrue(messageTransactionsData.isEmpty());
|
||||
|
||||
MessageTransaction transaction = testFeeNonce(repository, false, true, recipient, true);
|
||||
|
||||
// Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block
|
||||
assertFalse(transaction.isConfirmable());
|
||||
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
|
||||
assertFalse(isTransactionConfirmed(repository, transaction));
|
||||
assertEquals(12, transaction.getPoWDifficulty());
|
||||
|
||||
// Transaction should be found when trade bot searches for it
|
||||
messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, recipient, null, null, null);
|
||||
assertEquals(1, messageTransactionsData.size());
|
||||
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noRecipientNoFeeWithNonce() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
|
||||
MessageTransaction transaction = testFeeNonce(repository, false, true, null, true);
|
||||
|
||||
// Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block
|
||||
assertFalse(transaction.isConfirmable());
|
||||
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
|
||||
assertFalse(isTransactionConfirmed(repository, transaction));
|
||||
assertEquals(12, transaction.getPoWDifficulty());
|
||||
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atRecipientWithFeeNoNonce() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String atRecipient = deployAt();
|
||||
MessageTransaction transaction = testFeeNonce(repository, true, false, atRecipient, true);
|
||||
|
||||
// Transaction should be confirmable because it's to an AT, and therefore should be present in a block
|
||||
assertTrue(transaction.isConfirmable());
|
||||
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
|
||||
assertTrue(isTransactionConfirmed(repository, transaction));
|
||||
assertEquals(16, transaction.getPoWDifficulty());
|
||||
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regularRecipientWithFeeNoNonce() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
|
||||
MessageTransaction transaction = testFeeNonce(repository, true, false, recipient, true);
|
||||
|
||||
// Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block
|
||||
assertFalse(transaction.isConfirmable());
|
||||
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
|
||||
assertFalse(isTransactionConfirmed(repository, transaction));
|
||||
assertEquals(12, transaction.getPoWDifficulty());
|
||||
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atRecipientNoFeeWithNonceLegacyDifficulty() throws DataException, IllegalAccessException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Set mempowTransactionUpdatesTimestamp to a high value, so that it hasn't activated key
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "mempowTransactionUpdatesTimestamp", Long.MAX_VALUE, true);
|
||||
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String atRecipient = deployAt();
|
||||
MessageTransaction transaction = testFeeNonce(repository, false, true, atRecipient, true);
|
||||
|
||||
// Transaction should be confirmable because all MESSAGE transactions confirmed prior to the feature trigger
|
||||
assertTrue(transaction.isConfirmable());
|
||||
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
|
||||
assertTrue(isTransactionConfirmed(repository, transaction));
|
||||
assertEquals(14, transaction.getPoWDifficulty()); // Legacy difficulty was 14 in all cases
|
||||
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regularRecipientNoFeeWithNonceLegacyDifficulty() throws DataException, IllegalAccessException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Set mempowTransactionUpdatesTimestamp to a high value, so that it hasn't activated key
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "mempowTransactionUpdatesTimestamp", Long.MAX_VALUE, true);
|
||||
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
MessageTransaction transaction = testFeeNonce(repository, false, true, recipient, true);
|
||||
|
||||
// Transaction should be confirmable because all MESSAGE transactions confirmed prior to the feature trigger
|
||||
assertTrue(transaction.isConfirmable());
|
||||
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
|
||||
assertTrue(isTransactionConfirmed(repository, transaction)); // All MESSAGE transactions would confirm before feature trigger
|
||||
assertEquals(14, transaction.getPoWDifficulty()); // Legacy difficulty was 14 in all cases
|
||||
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializationTests() throws DataException, TransformationException {
|
||||
// with recipient, with amount
|
||||
@@ -165,6 +298,24 @@ public class MessageTests extends Common {
|
||||
testSerialization(null, 0L, null);
|
||||
}
|
||||
|
||||
private String deployAt() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
|
||||
String address = deployAtTransaction.getATAccount().getAddress();
|
||||
assertNotNull(address);
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTransactionConfirmed(Repository repository, MessageTransaction transaction) throws DataException {
|
||||
TransactionData queriedTransactionData = repository.getTransactionRepository().fromSignature(transaction.getTransactionData().getSignature());
|
||||
return queriedTransactionData.getBlockHeight() != null && queriedTransactionData.getBlockHeight() > 0;
|
||||
}
|
||||
|
||||
private boolean isValid(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TestAccount alice = Common.getTestAccount(repository, "alice");
|
||||
@@ -195,41 +346,48 @@ public class MessageTests extends Common {
|
||||
return messageTransaction.hasValidReference();
|
||||
}
|
||||
|
||||
private void testFeeNonce(boolean withFee, boolean withNonce, boolean isValid) throws DataException {
|
||||
|
||||
private MessageTransaction testFeeNonce(boolean withFee, boolean withNonce, boolean isValid) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TestAccount alice = Common.getTestAccount(repository, "alice");
|
||||
|
||||
int txGroupId = 0;
|
||||
int nonce = 0;
|
||||
long amount = 0;
|
||||
long assetId = Asset.QORT;
|
||||
byte[] data = new byte[1];
|
||||
boolean isText = false;
|
||||
boolean isEncrypted = false;
|
||||
|
||||
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
|
||||
version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
|
||||
|
||||
MessageTransaction transaction = new MessageTransaction(repository, transactionData);
|
||||
|
||||
if (withFee)
|
||||
transactionData.setFee(transaction.calcRecommendedFee());
|
||||
else
|
||||
transactionData.setFee(0L);
|
||||
|
||||
if (withNonce) {
|
||||
transaction.computeNonce();
|
||||
} else {
|
||||
transactionData.setNonce(-1);
|
||||
}
|
||||
|
||||
transaction.sign(alice);
|
||||
|
||||
assertEquals(isValid, transaction.isSignatureValid());
|
||||
return testFeeNonce(repository, withFee, withNonce, recipient, isValid);
|
||||
}
|
||||
}
|
||||
|
||||
private void testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
|
||||
private MessageTransaction testFeeNonce(Repository repository, boolean withFee, boolean withNonce, String recipient, boolean isValid) throws DataException {
|
||||
TestAccount alice = Common.getTestAccount(repository, "alice");
|
||||
|
||||
int txGroupId = 0;
|
||||
int nonce = 0;
|
||||
long amount = 0;
|
||||
long assetId = Asset.QORT;
|
||||
byte[] data = new byte[1];
|
||||
boolean isText = false;
|
||||
boolean isEncrypted = false;
|
||||
|
||||
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
|
||||
version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
|
||||
|
||||
MessageTransaction transaction = new MessageTransaction(repository, transactionData);
|
||||
|
||||
if (withFee)
|
||||
transactionData.setFee(transaction.calcRecommendedFee());
|
||||
else
|
||||
transactionData.setFee(0L);
|
||||
|
||||
if (withNonce) {
|
||||
transaction.computeNonce();
|
||||
} else {
|
||||
transactionData.setNonce(-1);
|
||||
}
|
||||
|
||||
transaction.sign(alice);
|
||||
|
||||
assertEquals(isValid, transaction.isSignatureValid());
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
private MessageTransaction testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TestAccount alice = Common.getTestAccount(repository, "alice");
|
||||
|
||||
@@ -244,6 +402,8 @@ public class MessageTests extends Common {
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
return new MessageTransaction(repository, transactionData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
@@ -24,6 +25,7 @@ import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -106,8 +108,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
||||
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
|
||||
title, description, tags, category);
|
||||
|
||||
// Check the chunk count is correct
|
||||
@@ -157,8 +160,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
||||
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
|
||||
title, description, tags, category);
|
||||
|
||||
// Check the chunk count is correct
|
||||
@@ -220,8 +224,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
||||
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
|
||||
title, description, tags, category);
|
||||
|
||||
// Check the chunk count is correct
|
||||
@@ -272,8 +277,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
||||
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
|
||||
title, description, tags, category);
|
||||
|
||||
// Check the chunk count is correct
|
||||
@@ -316,8 +322,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
||||
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
|
||||
title, description, tags, category);
|
||||
|
||||
// Check the metadata is correct
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.qortal.arbitrary.ArbitraryDataTransactionBuilder;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.PaymentData;
|
||||
@@ -50,51 +51,6 @@ public class ArbitraryTransactionTests extends Common {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDifficultyTooLow() throws IllegalAccessException, DataException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = null; // Not used for this test
|
||||
Service service = Service.ARBITRARY_DATA;
|
||||
int chunkSize = 100;
|
||||
int dataLength = 900; // Actual data length will be longer due to encryption
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize);
|
||||
|
||||
// Check that nonce validation succeeds
|
||||
byte[] signature = arbitraryDataFile.getSignature();
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData);
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
|
||||
// Increase difficulty to 15
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true);
|
||||
|
||||
// Make sure the nonce validation fails
|
||||
// Note: there is a very tiny chance this could succeed due to being extremely lucky
|
||||
// and finding a high difficulty nonce in the first couple of cycles. It will be rare
|
||||
// enough that we shouldn't need to account for it.
|
||||
assertFalse(transaction.isSignatureValid());
|
||||
|
||||
// Reduce difficulty back to 1, to double check
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonceAndFee() throws IllegalAccessException, DataException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -497,8 +453,9 @@ public class ArbitraryTransactionTests extends Common {
|
||||
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
|
||||
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
|
||||
null, null, null, null);
|
||||
|
||||
byte[] signature = arbitraryDataFile.getSignature();
|
||||
@@ -556,8 +513,9 @@ public class ArbitraryTransactionTests extends Common {
|
||||
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
|
||||
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
|
||||
title, description, tags, category);
|
||||
|
||||
byte[] signature = arbitraryDataFile.getSignature();
|
||||
@@ -614,8 +572,9 @@ public class ArbitraryTransactionTests extends Common {
|
||||
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
|
||||
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
|
||||
null, null, null, null);
|
||||
|
||||
byte[] signature = arbitraryDataFile.getSignature();
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package org.qortal.test.common;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.qortal.crypto.Qortal25519Extras.signForAggregation;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.Qortal25519Extras;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
@@ -107,6 +110,12 @@ public class AccountUtils {
|
||||
return sponsees;
|
||||
}
|
||||
|
||||
public static Account createRandomAccount(Repository repository) {
|
||||
byte[] randomPublicKey = new byte[32];
|
||||
new Random().nextBytes(randomPublicKey);
|
||||
return new PublicKeyAccount(repository, randomPublicKey);
|
||||
}
|
||||
|
||||
public static Transaction.ValidationResult createRandomRewardShare(Repository repository, PrivateKeyAccount account) throws DataException {
|
||||
// Bob attempts to create a reward share transaction
|
||||
byte[] randomPrivateKey = new byte[32];
|
||||
@@ -172,6 +181,24 @@ public class AccountUtils {
|
||||
assertEquals(String.format("%s's %s [%d] balance incorrect", accountName, assetName, assetId), expectedBalance, actualBalance);
|
||||
}
|
||||
|
||||
public static void assertBalanceGreaterThan(Repository repository, String accountName, long assetId, long minimumBalance) throws DataException {
|
||||
long actualBalance = getBalance(repository, accountName, assetId);
|
||||
String assetName = repository.getAssetRepository().fromAssetId(assetId).getName();
|
||||
|
||||
assertTrue(String.format("%s's %s [%d] balance incorrect", accountName, assetName, assetId), actualBalance > minimumBalance);
|
||||
}
|
||||
|
||||
|
||||
public static int getBlocksMinted(Repository repository, String accountName) throws DataException {
|
||||
return Common.getTestAccount(repository, accountName).getBlocksMinted();
|
||||
}
|
||||
|
||||
public static void assertBlocksMinted(Repository repository, String accountName, int expectedBlocksMinted) throws DataException {
|
||||
int actualBlocksMinted = getBlocksMinted(repository, accountName);
|
||||
|
||||
assertEquals(String.format("%s's blocks minted incorrect", accountName), expectedBlocksMinted, actualBlocksMinted);
|
||||
}
|
||||
|
||||
|
||||
public static List<OnlineAccountData> generateOnlineAccounts(int numAccounts) {
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>();
|
||||
|
||||
@@ -5,10 +5,12 @@ import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataTransactionBuilder;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
@@ -20,16 +22,15 @@ import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class ArbitraryUtils {
|
||||
|
||||
public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier,
|
||||
ArbitraryTransactionData.Method method, Service service, PrivateKeyAccount account,
|
||||
int chunkSize) throws DataException {
|
||||
|
||||
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
|
||||
return ArbitraryUtils.createAndMintTxn(repository, publicKey58, path, name, identifier, method, service,
|
||||
account, chunkSize, 0L, true, null, null, null, null);
|
||||
account, chunkSize, fee, false, null, null, null, null);
|
||||
}
|
||||
|
||||
public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier,
|
||||
@@ -47,7 +48,9 @@ public class ArbitraryUtils {
|
||||
}
|
||||
ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData();
|
||||
Transaction.ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, account);
|
||||
assertEquals(Transaction.ValidationResult.OK, result);
|
||||
if (result != Transaction.ValidationResult.OK) {
|
||||
throw new DataException(String.format("Arbitrary transaction invalid: %s", result.toString()));
|
||||
}
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We need a new ArbitraryDataFile instance because the files will have been moved to the signature's folder
|
||||
|
||||
@@ -10,6 +10,8 @@ import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BlockUtils {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockUtils.class);
|
||||
@@ -29,6 +31,20 @@ public class BlockUtils {
|
||||
return block;
|
||||
}
|
||||
|
||||
/** Mints a new block using "alice-reward-share" test account, via multiple re-orgs. */
|
||||
public static Block mintBlockWithReorgs(Repository repository, int reorgCount) throws DataException {
|
||||
PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share");
|
||||
Block block;
|
||||
|
||||
for (int i=0; i<reorgCount; i++) {
|
||||
block = BlockMinter.mintTestingBlock(repository, mintingAccount);
|
||||
assertNotNull(block);
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
}
|
||||
|
||||
return BlockMinter.mintTestingBlock(repository, mintingAccount);
|
||||
}
|
||||
|
||||
public static Long getNextBlockReward(Repository repository) throws DataException {
|
||||
int currentHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
@@ -70,4 +86,23 @@ public class BlockUtils {
|
||||
} while (true);
|
||||
}
|
||||
|
||||
public static void assertEqual(BlockData block1, BlockData block2) {
|
||||
assertArrayEquals(block1.getSignature(), block2.getSignature());
|
||||
assertEquals(block1.getVersion(), block2.getVersion());
|
||||
assertArrayEquals(block1.getReference(), block2.getReference());
|
||||
assertEquals(block1.getTransactionCount(), block2.getTransactionCount());
|
||||
assertEquals(block1.getTotalFees(), block2.getTotalFees());
|
||||
assertArrayEquals(block1.getTransactionsSignature(), block2.getTransactionsSignature());
|
||||
// assertEquals(block1.getHeight(), block2.getHeight()); // Height not automatically included after deserialization
|
||||
assertEquals(block1.getTimestamp(), block2.getTimestamp());
|
||||
assertArrayEquals(block1.getMinterPublicKey(), block2.getMinterPublicKey());
|
||||
assertArrayEquals(block1.getMinterSignature(), block2.getMinterSignature());
|
||||
assertEquals(block1.getATCount(), block2.getATCount());
|
||||
assertEquals(block1.getATFees(), block2.getATFees());
|
||||
assertArrayEquals(block1.getEncodedOnlineAccounts(), block2.getEncodedOnlineAccounts());
|
||||
assertEquals(block1.getOnlineAccountsCount(), block2.getOnlineAccountsCount());
|
||||
assertEquals(block1.getOnlineAccountsTimestamp(), block2.getOnlineAccountsTimestamp());
|
||||
assertArrayEquals(block1.getOnlineAccountsSignatures(), block2.getOnlineAccountsSignatures());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
790
src/test/java/org/qortal/test/crosschain/ACCTTests.java
Normal file
790
src/test/java/org/qortal/test/crosschain/ACCTTests.java
Normal file
@@ -0,0 +1,790 @@
|
||||
package org.qortal.test.crosschain;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
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.crosschain.ACCT;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public abstract class ACCTTests extends Common {
|
||||
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
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 foreignAmount = 864200L; // 0.00864200 foreign units
|
||||
|
||||
protected static final Random RANDOM = new Random();
|
||||
|
||||
protected abstract byte[] getPublicKey();
|
||||
|
||||
protected abstract byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout);
|
||||
|
||||
protected abstract ACCT getInstance();
|
||||
|
||||
protected abstract int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA);
|
||||
|
||||
protected abstract byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout);
|
||||
|
||||
protected abstract byte[] buildRedeemMessage(byte[] secretA, String address);
|
||||
|
||||
protected abstract byte[] getCodeBytesHash();
|
||||
|
||||
protected abstract String getSymbol();
|
||||
|
||||
protected abstract String getName();
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = buildQortalAT(tradeAccount.getAddress(), getPublicKey(), redeemAmount, foreignAmount, tradeTimeout);
|
||||
assertNotNull(creationBytes);
|
||||
|
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = fundingAmount;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = deployersInitialBalance;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = 0;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = getInstance().buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Instead of sending creator's address to AT, send too-short/invalid message
|
||||
byte[] messageData = new byte[7];
|
||||
RANDOM.nextBytes(messageData);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testTradingInfoProcessing() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check hashOfSecretA was extracted correctly
|
||||
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||
|
||||
// Check trade partner Qortal address was extracted correctly
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's Foreign Coin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(getPublicKey(), tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(AcctMode.OFFERING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
// Check refund
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account
|
||||
messageData = buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REDEEMED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageData = buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send incorrect secret to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32];
|
||||
RANDOM.nextBytes(wrongSecret);
|
||||
messageData = buildRedeemMessage(wrongSecret, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA);
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should be in TRADING mode
|
||||
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
for (ATData atData : executableAts) {
|
||||
String atAddress = atData.getATAddress();
|
||||
byte[] codeBytes = atData.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
|
||||
atAddress,
|
||||
codeBytes.length,
|
||||
(codeBytes.length != 1 ? "s": ""),
|
||||
HashCode.fromBytes(codeHash)));
|
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, getCodeBytesHash()))
|
||||
continue;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
protected DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
|
||||
byte[] creationBytes = buildQortalAT(tradeAddress, getPublicKey(), redeemAmount, foreignAmount, 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-" + getSymbol() + " cross-chain trade";
|
||||
String description = String.format("Qortal-" + getName() + " cross-chain trade");
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-" + getSymbol() + " 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;
|
||||
}
|
||||
|
||||
protected MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
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;
|
||||
}
|
||||
|
||||
protected void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
int refundTimeout = tradeTimeout / 2 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
}
|
||||
|
||||
protected void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
|
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tmode: %s\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected " + getName() + ": %s " + getSymbol() + ",\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
atData.getIsFinished(),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
describeRefundAt(tradeData, epochMilliFormatter);
|
||||
}
|
||||
|
||||
protected void describeRefundAt(CrossChainTradeData tradeData, Function<Long, String> epochMilliFormatter) {
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund timeout: %d minutes,\n"
|
||||
+ "\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
+ "\t" + getName() + " P2SH-A nLockTime: %d (%s),\n"
|
||||
+ "\ttrade partner: %s\n"
|
||||
+ "\tpartner's receiving address: %s",
|
||||
tradeData.refundTimeout,
|
||||
tradeData.tradeRefundHeight,
|
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||
tradeData.qortalPartnerAddress,
|
||||
tradeData.qortalPartnerReceivingAddress));
|
||||
}
|
||||
}
|
||||
|
||||
protected PrivateKeyAccount createTradeAccount(Repository repository) {
|
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice");
|
||||
}
|
||||
}
|
||||
@@ -1,121 +1,59 @@
|
||||
package org.qortal.test.crosschain;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
|
||||
public class BitcoinTests extends Common {
|
||||
public class BitcoinTests extends BitcoinyTests {
|
||||
|
||||
private Bitcoin bitcoin;
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings(); // TestNet3
|
||||
bitcoin = Bitcoin.getInstance();
|
||||
@Override
|
||||
protected String getCoinName() {
|
||||
return "Bitcoin";
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() {
|
||||
@Override
|
||||
protected String getCoinSymbol() {
|
||||
return "BTC";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitcoiny getCoin() {
|
||||
return Bitcoin.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetCoinForTesting() {
|
||||
Bitcoin.resetForTesting();
|
||||
bitcoin = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDeterministicKey58() {
|
||||
return "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getRecipient() {
|
||||
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
|
||||
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
|
||||
System.out.println(String.format("Starting BTC instance..."));
|
||||
System.out.println(String.format("BTC instance started"));
|
||||
|
||||
long before = System.currentTimeMillis();
|
||||
System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime()));
|
||||
long afterFirst = System.currentTimeMillis();
|
||||
|
||||
System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime()));
|
||||
long afterSecond = System.currentTimeMillis();
|
||||
|
||||
long firstPeriod = afterFirst - before;
|
||||
long secondPeriod = afterSecond - afterFirst;
|
||||
|
||||
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
|
||||
|
||||
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
|
||||
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
|
||||
}
|
||||
public void testGetMedianBlockTime() {}
|
||||
|
||||
@Test
|
||||
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
|
||||
public void testFindHtlcSecret() throws ForeignBlockchainException {
|
||||
// This actually exists on TEST3 but can take a while to fetch
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
|
||||
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||
byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress);
|
||||
|
||||
assertNotNull(secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
public void testFindHtlcSecret() {}
|
||||
|
||||
@Test
|
||||
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
|
||||
public void testBuildSpend() {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
long amount = 1000L;
|
||||
|
||||
Transaction transaction = bitcoin.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull(transaction);
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
transaction = bitcoin.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull(transaction);
|
||||
}
|
||||
public void testBuildSpend() {}
|
||||
|
||||
@Test
|
||||
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
Long balance = bitcoin.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(balance);
|
||||
|
||||
System.out.println(bitcoin.format(balance));
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
Long repeatBalance = bitcoin.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(repeatBalance);
|
||||
|
||||
System.out.println(bitcoin.format(repeatBalance));
|
||||
|
||||
assertEquals(balance, repeatBalance);
|
||||
}
|
||||
public void testGetWalletBalance() {}
|
||||
|
||||
@Test
|
||||
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
|
||||
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
String address = bitcoin.getUnusedReceiveAddress(xprv58);
|
||||
|
||||
assertNotNull(address);
|
||||
|
||||
System.out.println(address);
|
||||
}
|
||||
|
||||
public void testGetUnusedReceiveAddress() {}
|
||||
}
|
||||
|
||||
130
src/test/java/org/qortal/test/crosschain/BitcoinyTests.java
Normal file
130
src/test/java/org/qortal/test/crosschain/BitcoinyTests.java
Normal file
@@ -0,0 +1,130 @@
|
||||
package org.qortal.test.crosschain;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public abstract class BitcoinyTests extends Common {
|
||||
|
||||
protected Bitcoiny bitcoiny;
|
||||
|
||||
protected abstract String getCoinName();
|
||||
|
||||
protected abstract String getCoinSymbol();
|
||||
|
||||
protected abstract Bitcoiny getCoin();
|
||||
|
||||
protected abstract void resetCoinForTesting();
|
||||
|
||||
protected abstract String getDeterministicKey58();
|
||||
|
||||
protected abstract String getRecipient();
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings(); // TestNet3
|
||||
bitcoiny = getCoin();
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() {
|
||||
resetCoinForTesting();
|
||||
bitcoiny = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMedianBlockTime() throws ForeignBlockchainException {
|
||||
System.out.println(String.format("Starting " + getCoinSymbol() + " instance..."));
|
||||
System.out.println(String.format(getCoinSymbol() + " instance started"));
|
||||
|
||||
long before = System.currentTimeMillis();
|
||||
System.out.println(String.format(getCoinName() + " median blocktime: %d", bitcoiny.getMedianBlockTime()));
|
||||
long afterFirst = System.currentTimeMillis();
|
||||
|
||||
System.out.println(String.format(getCoinName() + " median blocktime: %d", bitcoiny.getMedianBlockTime()));
|
||||
long afterSecond = System.currentTimeMillis();
|
||||
|
||||
long firstPeriod = afterFirst - before;
|
||||
long secondPeriod = afterSecond - afterFirst;
|
||||
|
||||
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
|
||||
|
||||
makeGetMedianBlockTimeAssertions(firstPeriod, secondPeriod);
|
||||
}
|
||||
|
||||
public void makeGetMedianBlockTimeAssertions(long firstPeriod, long secondPeriod) {
|
||||
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
|
||||
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindHtlcSecret() throws ForeignBlockchainException {
|
||||
// This actually exists on TEST3 but can take a while to fetch
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
|
||||
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||
byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoiny, p2shAddress);
|
||||
|
||||
assertNotNull(secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildSpend() {
|
||||
String xprv58 = getDeterministicKey58();
|
||||
|
||||
String recipient = getRecipient();
|
||||
long amount = 1000L;
|
||||
|
||||
Transaction transaction = bitcoiny.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull(transaction);
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
transaction = bitcoiny.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull(transaction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = getDeterministicKey58();
|
||||
|
||||
Long balance = bitcoiny.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(balance);
|
||||
|
||||
System.out.println(bitcoiny.format(balance));
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
Long repeatBalance = bitcoiny.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(repeatBalance);
|
||||
|
||||
System.out.println(bitcoiny.format(repeatBalance));
|
||||
|
||||
assertEquals(balance, repeatBalance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
|
||||
String xprv58 = getDeterministicKey58();
|
||||
|
||||
String address = bitcoiny.getUnusedReceiveAddress(xprv58);
|
||||
|
||||
assertNotNull(address);
|
||||
|
||||
System.out.println(address);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +1,48 @@
|
||||
package org.qortal.test.crosschain;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.Digibyte;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
public class DigibyteTests extends Common {
|
||||
public class DigibyteTests extends BitcoinyTests {
|
||||
|
||||
private Digibyte digibyte;
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings(); // TestNet3
|
||||
digibyte = Digibyte.getInstance();
|
||||
@Override
|
||||
protected String getCoinName() {
|
||||
return "Digibyte";
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() {
|
||||
@Override
|
||||
protected String getCoinSymbol() {
|
||||
return "DGB";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitcoiny getCoin() {
|
||||
return Digibyte.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetCoinForTesting() {
|
||||
Digibyte.resetForTesting();
|
||||
digibyte = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
|
||||
long before = System.currentTimeMillis();
|
||||
System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
|
||||
long afterFirst = System.currentTimeMillis();
|
||||
@Override
|
||||
protected String getDeterministicKey58() {
|
||||
return "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
|
||||
}
|
||||
|
||||
System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
|
||||
long afterSecond = System.currentTimeMillis();
|
||||
|
||||
long firstPeriod = afterFirst - before;
|
||||
long secondPeriod = afterSecond - afterFirst;
|
||||
|
||||
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
|
||||
|
||||
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
|
||||
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
|
||||
@Override
|
||||
protected String getRecipient() {
|
||||
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "Doesn't work, to be fixed later")
|
||||
public void testFindHtlcSecret() throws ForeignBlockchainException {
|
||||
// This actually exists on TEST3 but can take a while to fetch
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
|
||||
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||
byte[] secret = BitcoinyHTLC.findHtlcSecret(digibyte, p2shAddress);
|
||||
|
||||
assertNotNull("secret not found", secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
public void testFindHtlcSecret() {}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
|
||||
public void testBuildSpend() {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
public void testBuildSpend() {}
|
||||
|
||||
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
long amount = 1000L;
|
||||
|
||||
Transaction transaction = digibyte.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull("insufficient funds", transaction);
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
transaction = digibyte.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull("insufficient funds", transaction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
|
||||
|
||||
Long balance = digibyte.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(balance);
|
||||
|
||||
System.out.println(digibyte.format(balance));
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
Long repeatBalance = digibyte.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(repeatBalance);
|
||||
|
||||
System.out.println(digibyte.format(repeatBalance));
|
||||
|
||||
assertEquals(balance, repeatBalance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
|
||||
String xprv58 = "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
|
||||
|
||||
String address = digibyte.getUnusedReceiveAddress(xprv58);
|
||||
|
||||
assertNotNull(address);
|
||||
|
||||
System.out.println(address);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,115 +1,47 @@
|
||||
package org.qortal.test.crosschain;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.Dogecoin;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
import java.util.Arrays;
|
||||
public class DogecoinTests extends BitcoinyTests {
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class DogecoinTests extends Common {
|
||||
|
||||
private Dogecoin dogecoin;
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings(); // TestNet3
|
||||
dogecoin = Dogecoin.getInstance();
|
||||
@Override
|
||||
protected String getCoinName() {
|
||||
return "Dogecoin";
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() {
|
||||
@Override
|
||||
protected String getCoinSymbol() {
|
||||
return "DOGE";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitcoiny getCoin() {
|
||||
return Dogecoin.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetCoinForTesting() {
|
||||
Dogecoin.resetForTesting();
|
||||
dogecoin = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
|
||||
long before = System.currentTimeMillis();
|
||||
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime()));
|
||||
long afterFirst = System.currentTimeMillis();
|
||||
@Override
|
||||
protected String getDeterministicKey58() {
|
||||
return "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
|
||||
}
|
||||
|
||||
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime()));
|
||||
long afterSecond = System.currentTimeMillis();
|
||||
|
||||
long firstPeriod = afterFirst - before;
|
||||
long secondPeriod = afterSecond - afterFirst;
|
||||
|
||||
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
|
||||
|
||||
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
|
||||
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
|
||||
@Override
|
||||
protected String getRecipient() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "Doesn't work, to be fixed later")
|
||||
public void testFindHtlcSecret() throws ForeignBlockchainException {
|
||||
// This actually exists on TEST3 but can take a while to fetch
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
|
||||
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||
byte[] secret = BitcoinyHTLC.findHtlcSecret(dogecoin, p2shAddress);
|
||||
|
||||
assertNotNull("secret not found", secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
public void testFindHtlcSecret() {}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
|
||||
public void testBuildSpend() {
|
||||
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
|
||||
|
||||
String recipient = "DP1iFao33xdEPa5vaArpj7sykfzKNeiJeX";
|
||||
long amount = 1000L;
|
||||
|
||||
Transaction transaction = dogecoin.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull("insufficient funds", transaction);
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
transaction = dogecoin.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull("insufficient funds", transaction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
|
||||
|
||||
Long balance = dogecoin.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(balance);
|
||||
|
||||
System.out.println(dogecoin.format(balance));
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
Long repeatBalance = dogecoin.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(repeatBalance);
|
||||
|
||||
System.out.println(dogecoin.format(repeatBalance));
|
||||
|
||||
assertEquals(balance, repeatBalance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
|
||||
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
|
||||
|
||||
String address = dogecoin.getUnusedReceiveAddress(xprv58);
|
||||
|
||||
assertNotNull(address);
|
||||
|
||||
System.out.println(address);
|
||||
}
|
||||
|
||||
public void testBuildSpend() {}
|
||||
}
|
||||
|
||||
@@ -1,113 +1,43 @@
|
||||
package org.qortal.test.crosschain;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
public class LitecoinTests extends Common {
|
||||
public class LitecoinTests extends BitcoinyTests {
|
||||
|
||||
private Litecoin litecoin;
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings(); // TestNet3
|
||||
litecoin = Litecoin.getInstance();
|
||||
@Override
|
||||
protected String getCoinName() {
|
||||
return "Litecoin";
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() {
|
||||
@Override
|
||||
protected String getCoinSymbol() {
|
||||
return "LTC";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitcoiny getCoin() {
|
||||
return Litecoin.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetCoinForTesting() {
|
||||
Litecoin.resetForTesting();
|
||||
litecoin = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMedianBlockTime() throws ForeignBlockchainException {
|
||||
long before = System.currentTimeMillis();
|
||||
System.out.println(String.format("Litecoin median blocktime: %d", litecoin.getMedianBlockTime()));
|
||||
long afterFirst = System.currentTimeMillis();
|
||||
@Override
|
||||
protected String getDeterministicKey58() {
|
||||
return "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
}
|
||||
|
||||
System.out.println(String.format("Litecoin median blocktime: %d", litecoin.getMedianBlockTime()));
|
||||
long afterSecond = System.currentTimeMillis();
|
||||
|
||||
long firstPeriod = afterFirst - before;
|
||||
long secondPeriod = afterSecond - afterFirst;
|
||||
|
||||
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
|
||||
|
||||
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
|
||||
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
|
||||
@Override
|
||||
protected String getRecipient() {
|
||||
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "Doesn't work, to be fixed later")
|
||||
public void testFindHtlcSecret() throws ForeignBlockchainException {
|
||||
// This actually exists on TEST3 but can take a while to fetch
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
|
||||
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||
byte[] secret = BitcoinyHTLC.findHtlcSecret(litecoin, p2shAddress);
|
||||
|
||||
assertNotNull("secret not found", secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildSpend() {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
long amount = 1000L;
|
||||
|
||||
Transaction transaction = litecoin.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull("insufficient funds", transaction);
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
transaction = litecoin.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull("insufficient funds", transaction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
Long balance = litecoin.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(balance);
|
||||
|
||||
System.out.println(litecoin.format(balance));
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
Long repeatBalance = litecoin.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(repeatBalance);
|
||||
|
||||
System.out.println(litecoin.format(repeatBalance));
|
||||
|
||||
assertEquals(balance, repeatBalance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
String address = litecoin.getUnusedReceiveAddress(xprv58);
|
||||
|
||||
assertNotNull(address);
|
||||
|
||||
System.out.println(address);
|
||||
}
|
||||
|
||||
public void testFindHtlcSecret() {}
|
||||
}
|
||||
|
||||
@@ -3,57 +3,53 @@ package org.qortal.test.crosschain;
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats.*;
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.transform.TransformationException;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.qortal.crosschain.BitcoinyHTLC.Status.*;
|
||||
|
||||
public class PirateChainTests extends Common {
|
||||
public class PirateChainTests extends BitcoinyTests {
|
||||
|
||||
private PirateChain pirateChain;
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
pirateChain = PirateChain.getInstance();
|
||||
@Override
|
||||
protected String getCoinName() {
|
||||
return "PirateChain";
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() {
|
||||
@Override
|
||||
protected String getCoinSymbol() {
|
||||
return "ARRR";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitcoiny getCoin() {
|
||||
return PirateChain.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetCoinForTesting() {
|
||||
Litecoin.resetForTesting();
|
||||
pirateChain = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
|
||||
long before = System.currentTimeMillis();
|
||||
System.out.println(String.format("Pirate Chain median blocktime: %d", pirateChain.getMedianBlockTime()));
|
||||
long afterFirst = System.currentTimeMillis();
|
||||
@Override
|
||||
protected String getDeterministicKey58() {
|
||||
return null;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Pirate Chain median blocktime: %d", pirateChain.getMedianBlockTime()));
|
||||
long afterSecond = System.currentTimeMillis();
|
||||
@Override
|
||||
protected String getRecipient() {
|
||||
return null;
|
||||
}
|
||||
|
||||
long firstPeriod = afterFirst - before;
|
||||
long secondPeriod = afterSecond - afterFirst;
|
||||
|
||||
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
|
||||
|
||||
assertTrue("1st call should take less than 5 seconds", firstPeriod < 5000L);
|
||||
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
|
||||
public void makeGetMedianBlockTimeAssertions(long firstPeriod, long secondPeriod) {
|
||||
assertTrue("1st call should take less than 5 seconds", firstPeriod < 5000L);
|
||||
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -62,7 +58,7 @@ public class PirateChainTests extends Common {
|
||||
int count = 20;
|
||||
|
||||
long before = System.currentTimeMillis();
|
||||
List<CompactBlock> compactBlocks = pirateChain.getCompactBlocks(startHeight, count);
|
||||
List<CompactBlock> compactBlocks = ((PirateChain) bitcoiny).getCompactBlocks(startHeight, count);
|
||||
long after = System.currentTimeMillis();
|
||||
|
||||
System.out.println(String.format("Retrieval took: %d ms", after-before));
|
||||
@@ -82,7 +78,7 @@ public class PirateChainTests extends Common {
|
||||
Bytes.reverse(txBytes);
|
||||
String txHashBE = HashCode.fromBytes(txBytes).toString();
|
||||
|
||||
byte[] rawTransaction = pirateChain.getBlockchainProvider().getRawTransaction(txHashBE);
|
||||
byte[] rawTransaction = bitcoiny.getBlockchainProvider().getRawTransaction(txHashBE);
|
||||
assertNotNull(rawTransaction);
|
||||
}
|
||||
|
||||
@@ -121,7 +117,7 @@ public class PirateChainTests extends Common {
|
||||
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
|
||||
long p2shFee = 10000;
|
||||
final long minimumAmount = 10000 + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount);
|
||||
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
|
||||
assertEquals(FUNDED, htlcStatus);
|
||||
}
|
||||
|
||||
@@ -130,7 +126,7 @@ public class PirateChainTests extends Common {
|
||||
String p2shAddress = "bYZrzSSgGp8aEGvihukoMGU8sXYrx19Wka";
|
||||
long p2shFee = 10000;
|
||||
final long minimumAmount = 10000 + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount);
|
||||
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
|
||||
assertEquals(REDEEMED, htlcStatus);
|
||||
}
|
||||
|
||||
@@ -139,14 +135,14 @@ public class PirateChainTests extends Common {
|
||||
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv";
|
||||
long p2shFee = 10000;
|
||||
final long minimumAmount = 10000 + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount);
|
||||
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
|
||||
assertEquals(REFUNDED, htlcStatus);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTxidForUnspentAddress() throws ForeignBlockchainException {
|
||||
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
|
||||
String txid = PirateChainHTLC.getFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress);
|
||||
String txid = PirateChainHTLC.getFundingTxid(bitcoiny.getBlockchainProvider(), p2shAddress);
|
||||
|
||||
// Reverse the byte order of the txid used by block explorers, to get to big-endian form
|
||||
byte[] expectedTxidLE = HashCode.fromString("fea4b0c1abcf8f0f3ddc2fa2f9438501ee102aad62a9ff18a5ce7d08774755c0").asBytes();
|
||||
@@ -161,7 +157,7 @@ public class PirateChainTests extends Common {
|
||||
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
|
||||
long p2shFee = 10000;
|
||||
final long minimumAmount = 10000 + p2shFee;
|
||||
String txid = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount);
|
||||
String txid = PirateChainHTLC.getUnspentFundingTxid(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
|
||||
|
||||
// Reverse the byte order of the txid used by block explorers, to get to big-endian form
|
||||
byte[] expectedTxidLE = HashCode.fromString("fea4b0c1abcf8f0f3ddc2fa2f9438501ee102aad62a9ff18a5ce7d08774755c0").asBytes();
|
||||
@@ -174,7 +170,7 @@ public class PirateChainTests extends Common {
|
||||
@Test
|
||||
public void testGetTxidForSpentAddress() throws ForeignBlockchainException {
|
||||
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; //"t3KtVxeEb8srJofo6atMEpMpEP6TjEi8VqA";
|
||||
String txid = PirateChainHTLC.getFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress);
|
||||
String txid = PirateChainHTLC.getFundingTxid(bitcoiny.getBlockchainProvider(), p2shAddress);
|
||||
|
||||
// Reverse the byte order of the txid used by block explorers, to get to big-endian form
|
||||
byte[] expectedTxidLE = HashCode.fromString("fb386fc8eea0fbf3ea37047726b92c39441652b32d8d62a274331687f7a1eca8").asBytes();
|
||||
@@ -187,7 +183,7 @@ public class PirateChainTests extends Common {
|
||||
@Test
|
||||
public void testGetTransactionsForAddress() throws ForeignBlockchainException {
|
||||
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; //"t3KtVxeEb8srJofo6atMEpMpEP6TjEi8VqA";
|
||||
List<BitcoinyTransaction> transactions = pirateChain.getBlockchainProvider()
|
||||
List<BitcoinyTransaction> transactions = bitcoiny.getBlockchainProvider()
|
||||
.getAddressBitcoinyTransactions(p2shAddress, false);
|
||||
|
||||
assertEquals(2, transactions.size());
|
||||
@@ -232,66 +228,17 @@ public class PirateChainTests extends Common {
|
||||
|
||||
@Test
|
||||
@Ignore(value = "Doesn't work, to be fixed later")
|
||||
public void testFindHtlcSecret() throws ForeignBlockchainException {
|
||||
// This actually exists on TEST3 but can take a while to fetch
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
|
||||
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||
byte[] secret = BitcoinyHTLC.findHtlcSecret(pirateChain, p2shAddress);
|
||||
|
||||
assertNotNull("secret not found", secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
public void testFindHtlcSecret() {}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "Needs adapting for Pirate Chain")
|
||||
public void testBuildSpend() {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
long amount = 1000L;
|
||||
|
||||
Transaction transaction = pirateChain.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull("insufficient funds", transaction);
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
transaction = pirateChain.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull("insufficient funds", transaction);
|
||||
}
|
||||
public void testBuildSpend() {}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "Needs adapting for Pirate Chain")
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
Long balance = pirateChain.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(balance);
|
||||
|
||||
System.out.println(pirateChain.format(balance));
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
Long repeatBalance = pirateChain.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(repeatBalance);
|
||||
|
||||
System.out.println(pirateChain.format(repeatBalance));
|
||||
|
||||
assertEquals(balance, repeatBalance);
|
||||
}
|
||||
public void testGetWalletBalance() {}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "Needs adapting for Pirate Chain")
|
||||
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
String address = pirateChain.getUnusedReceiveAddress(xprv58);
|
||||
|
||||
assertNotNull(address);
|
||||
|
||||
System.out.println(address);
|
||||
}
|
||||
|
||||
}
|
||||
public void testGetUnusedReceiveAddress() {}
|
||||
}
|
||||
@@ -1,115 +1,47 @@
|
||||
package org.qortal.test.crosschain;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.Ravencoin;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
public class RavencoinTests extends Common {
|
||||
public class RavencoinTests extends BitcoinyTests {
|
||||
|
||||
private Ravencoin ravencoin;
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings(); // TestNet3
|
||||
ravencoin = Ravencoin.getInstance();
|
||||
@Override
|
||||
protected String getCoinName() {
|
||||
return "Ravencoin";
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() {
|
||||
@Override
|
||||
protected String getCoinSymbol() {
|
||||
return "RVN";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitcoiny getCoin() {
|
||||
return Ravencoin.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetCoinForTesting() {
|
||||
Ravencoin.resetForTesting();
|
||||
ravencoin = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
|
||||
long before = System.currentTimeMillis();
|
||||
System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime()));
|
||||
long afterFirst = System.currentTimeMillis();
|
||||
@Override
|
||||
protected String getDeterministicKey58() {
|
||||
return "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
|
||||
}
|
||||
|
||||
System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime()));
|
||||
long afterSecond = System.currentTimeMillis();
|
||||
|
||||
long firstPeriod = afterFirst - before;
|
||||
long secondPeriod = afterSecond - afterFirst;
|
||||
|
||||
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
|
||||
|
||||
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
|
||||
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
|
||||
@Override
|
||||
protected String getRecipient() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "Doesn't work, to be fixed later")
|
||||
public void testFindHtlcSecret() throws ForeignBlockchainException {
|
||||
// This actually exists on TEST3 but can take a while to fetch
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
|
||||
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||
byte[] secret = BitcoinyHTLC.findHtlcSecret(ravencoin, p2shAddress);
|
||||
|
||||
assertNotNull("secret not found", secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
public void testFindHtlcSecret() {}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
|
||||
public void testBuildSpend() {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
long amount = 1000L;
|
||||
|
||||
Transaction transaction = ravencoin.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull("insufficient funds", transaction);
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
transaction = ravencoin.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull("insufficient funds", transaction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
|
||||
|
||||
Long balance = ravencoin.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(balance);
|
||||
|
||||
System.out.println(ravencoin.format(balance));
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
Long repeatBalance = ravencoin.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(repeatBalance);
|
||||
|
||||
System.out.println(ravencoin.format(repeatBalance));
|
||||
|
||||
assertEquals(balance, repeatBalance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
|
||||
String xprv58 = "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
|
||||
|
||||
String address = ravencoin.getUnusedReceiveAddress(xprv58);
|
||||
|
||||
assertNotNull(address);
|
||||
|
||||
System.out.println(address);
|
||||
}
|
||||
|
||||
public void testBuildSpend() {}
|
||||
}
|
||||
|
||||
@@ -2,507 +2,89 @@ package org.qortal.test.crosschain.bitcoinv1;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
|
||||
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.crosschain.ACCT;
|
||||
import org.qortal.crosschain.BitcoinACCTv1;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.crosschain.ACCTTests;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
public class BitcoinACCTv1Tests extends Common {
|
||||
public class BitcoinACCTv1Tests extends ACCTTests {
|
||||
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final byte[] secretB = "This string is roughly 32 bytes?".getBytes();
|
||||
public static final byte[] hashOfSecretB = Crypto.hash160(secretB); // 31f0dd71decf59bbc8ef0661f4030479255cfa58
|
||||
public static final byte[] bitcoinPublicKeyHash = 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 bitcoinAmount = 864200L; // 0.00864200 BTC
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
private static final String SYMBOL = "BTC";
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
private static final String NAME = "Bitcoin";
|
||||
|
||||
@Override
|
||||
protected byte[] getPublicKey() {
|
||||
return bitcoinPublicKeyHash;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
|
||||
assertNotNull(creationBytes);
|
||||
|
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
@Override
|
||||
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
|
||||
return BitcoinACCTv1.buildQortalAT(address,publicKey, hashOfSecretB, redeemAmount, foreignAmount,tradeTimeout);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
@Override
|
||||
protected ACCT getInstance() {
|
||||
return BitcoinACCTv1.getInstance();
|
||||
}
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
@Override
|
||||
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
|
||||
return BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
}
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
@Override
|
||||
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||
return BitcoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
}
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
@Override
|
||||
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
|
||||
return BitcoinACCTv1.buildRedeemMessage(secretA,secretB,address);
|
||||
}
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
@Override
|
||||
protected byte[] getCodeBytesHash() {
|
||||
return BitcoinACCTv1.CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
@Override
|
||||
protected String getSymbol() {
|
||||
return SYMBOL;
|
||||
}
|
||||
|
||||
expectedBalance = fundingAmount;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = deployersInitialBalance;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = 0;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
@Test
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = BitcoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Instead of sending creator's address to AT, send too-short/invalid message
|
||||
byte[] messageData = new byte[7];
|
||||
RANDOM.nextBytes(messageData);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testTradingInfoProcessing() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check hashOfSecretA was extracted correctly
|
||||
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||
|
||||
// Check trade partner Qortal address was extracted correctly
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's Bitcoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(AcctMode.OFFERING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
// Check refund
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretsCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secrets to AT, from correct account
|
||||
messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REDEEMED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretsIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secrets to AT, but from wrong account
|
||||
messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretsCorrectSender() throws DataException {
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
@@ -582,197 +164,8 @@ public class BitcoinACCTv1Tests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretsCorrectSenderInvalidMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secrets to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA, secretB);
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should be in TRADING mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
for (ATData atData : executableAts) {
|
||||
String atAddress = atData.getATAddress();
|
||||
byte[] codeBytes = atData.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
|
||||
atAddress,
|
||||
codeBytes.length,
|
||||
(codeBytes.length != 1 ? "s": ""),
|
||||
HashCode.fromBytes(codeHash)));
|
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, BitcoinACCTv1.CODE_BYTES_HASH))
|
||||
continue;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
|
||||
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, 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-BTC cross-chain trade";
|
||||
String description = String.format("Qortal-Bitcoin cross-chain trade");
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-BTC 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) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
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;
|
||||
}
|
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
int refundTimeout = tradeTimeout * 3 / 4 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
}
|
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tmode: %s\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tHASH160 of secret-B: %s,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected bitcoin: %s BTC,\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
atData.getIsFinished(),
|
||||
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
@Override
|
||||
protected void describeRefundAt(CrossChainTradeData tradeData, Function<Long, String> epochMilliFormatter) {
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
@@ -786,10 +179,4 @@ public class BitcoinACCTv1Tests extends Common {
|
||||
tradeData.qortalPartnerAddress));
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKeyAccount createTradeAccount(Repository repository) {
|
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,769 +1,58 @@
|
||||
package org.qortal.test.crosschain.bitcoinv3;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
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.crosschain.AcctMode;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.BitcoinACCTv3;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.test.crosschain.ACCTTests;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
public class BitcoinACCTv3Tests extends ACCTTests {
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BitcoinACCTv3Tests extends Common {
|
||||
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final byte[] bitcoinPublicKeyHash = 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 bitcoinAmount = 864200L; // 0.00864200 BTC
|
||||
private static final String SYMBOL = "BTC";
|
||||
private static final String NAME = "Bitcoin";
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
@Override
|
||||
protected byte[] getPublicKey() {
|
||||
return bitcoinPublicKeyHash;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = BitcoinACCTv3.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, redeemAmount, bitcoinAmount, tradeTimeout);
|
||||
assertNotNull(creationBytes);
|
||||
|
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
@Override
|
||||
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
|
||||
return BitcoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = fundingAmount;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = deployersInitialBalance;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = 0;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected ACCT getInstance() {
|
||||
return BitcoinACCTv3.getInstance();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = BitcoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
|
||||
return BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Instead of sending creator's address to AT, send too-short/invalid message
|
||||
byte[] messageData = new byte[7];
|
||||
RANDOM.nextBytes(messageData);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||
return BitcoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testTradingInfoProcessing() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check hashOfSecretA was extracted correctly
|
||||
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||
|
||||
// Check trade partner Qortal address was extracted correctly
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's Bitcoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
|
||||
return BitcoinACCTv3.buildRedeemMessage(secretA, address);
|
||||
}
|
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(AcctMode.OFFERING, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] getCodeBytesHash() {
|
||||
return BitcoinACCTv3.CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
// Check refund
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected String getSymbol() {
|
||||
return SYMBOL;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account
|
||||
messageData = BitcoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REDEEMED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
|
||||
}
|
||||
@Override
|
||||
protected String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageData = BitcoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send incorrect secret to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32];
|
||||
RANDOM.nextBytes(wrongSecret);
|
||||
messageData = BitcoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA);
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should be in TRADING mode
|
||||
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
for (ATData atData : executableAts) {
|
||||
String atAddress = atData.getATAddress();
|
||||
byte[] codeBytes = atData.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
|
||||
atAddress,
|
||||
codeBytes.length,
|
||||
(codeBytes.length != 1 ? "s": ""),
|
||||
HashCode.fromBytes(codeHash)));
|
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, BitcoinACCTv3.CODE_BYTES_HASH))
|
||||
continue;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
|
||||
byte[] creationBytes = BitcoinACCTv3.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, redeemAmount, bitcoinAmount, 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-BTC cross-chain trade";
|
||||
String description = String.format("Qortal-Bitcoin cross-chain trade");
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-BTC 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) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
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;
|
||||
}
|
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
int refundTimeout = tradeTimeout / 2 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
}
|
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tmode: %s\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected Bitcoin: %s BTC,\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
atData.getIsFinished(),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund timeout: %d minutes,\n"
|
||||
+ "\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n"
|
||||
+ "\ttrade partner: %s\n"
|
||||
+ "\tpartner's receiving address: %s",
|
||||
tradeData.refundTimeout,
|
||||
tradeData.tradeRefundHeight,
|
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||
tradeData.qortalPartnerAddress,
|
||||
tradeData.qortalPartnerReceivingAddress));
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKeyAccount createTradeAccount(Repository repository) {
|
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,769 +1,58 @@
|
||||
package org.qortal.test.crosschain.digibytev3;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
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.crosschain.AcctMode;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.DigibyteACCTv3;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.test.crosschain.ACCTTests;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
public class DigibyteACCTv3Tests extends ACCTTests {
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class DigibyteACCTv3Tests extends Common {
|
||||
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final byte[] digibytePublicKeyHash = 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 digibyteAmount = 864200L; // 0.00864200 DGB
|
||||
private static final String SYMBOL = "DGB";
|
||||
private static final String NAME = "DigiByte";
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
@Override
|
||||
protected byte[] getPublicKey() {
|
||||
return digibytePublicKeyHash;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAccount.getAddress(), digibytePublicKeyHash, redeemAmount, digibyteAmount, tradeTimeout);
|
||||
assertNotNull(creationBytes);
|
||||
|
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
@Override
|
||||
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
|
||||
return DigibyteACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = fundingAmount;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = deployersInitialBalance;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = 0;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected ACCT getInstance() {
|
||||
return DigibyteACCTv3.getInstance();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = DigibyteACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
|
||||
return DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Instead of sending creator's address to AT, send too-short/invalid message
|
||||
byte[] messageData = new byte[7];
|
||||
RANDOM.nextBytes(messageData);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||
return DigibyteACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testTradingInfoProcessing() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check hashOfSecretA was extracted correctly
|
||||
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||
|
||||
// Check trade partner Qortal address was extracted correctly
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's digibyte PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(digibytePublicKeyHash, tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
|
||||
return DigibyteACCTv3.buildRedeemMessage(secretA, address);
|
||||
}
|
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(AcctMode.OFFERING, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] getCodeBytesHash() {
|
||||
return DigibyteACCTv3.CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
// Check refund
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected String getSymbol() {
|
||||
return SYMBOL;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account
|
||||
messageData = DigibyteACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REDEEMED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
|
||||
}
|
||||
@Override
|
||||
protected String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageData = DigibyteACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send incorrect secret to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32];
|
||||
RANDOM.nextBytes(wrongSecret);
|
||||
messageData = DigibyteACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA);
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should be in TRADING mode
|
||||
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
for (ATData atData : executableAts) {
|
||||
String atAddress = atData.getATAddress();
|
||||
byte[] codeBytes = atData.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
|
||||
atAddress,
|
||||
codeBytes.length,
|
||||
(codeBytes.length != 1 ? "s": ""),
|
||||
HashCode.fromBytes(codeHash)));
|
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, DigibyteACCTv3.CODE_BYTES_HASH))
|
||||
continue;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
|
||||
byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAddress, digibytePublicKeyHash, redeemAmount, digibyteAmount, 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-DGB cross-chain trade";
|
||||
String description = String.format("Qortal-Digibyte cross-chain trade");
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-DGB 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) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
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;
|
||||
}
|
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
int refundTimeout = tradeTimeout / 2 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
}
|
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tmode: %s\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected digibyte: %s DGB,\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
atData.getIsFinished(),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund timeout: %d minutes,\n"
|
||||
+ "\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
+ "\tDigibyte P2SH-A nLockTime: %d (%s),\n"
|
||||
+ "\ttrade partner: %s\n"
|
||||
+ "\tpartner's receiving address: %s",
|
||||
tradeData.refundTimeout,
|
||||
tradeData.tradeRefundHeight,
|
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||
tradeData.qortalPartnerAddress,
|
||||
tradeData.qortalPartnerReceivingAddress));
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKeyAccount createTradeAccount(Repository repository) {
|
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,769 +1,58 @@
|
||||
package org.qortal.test.crosschain.dogecoinv3;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
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.crosschain.AcctMode;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.DogecoinACCTv3;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.test.crosschain.ACCTTests;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
public class DogecoinACCTv3Tests extends ACCTTests {
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class DogecoinACCTv3Tests extends Common {
|
||||
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final byte[] dogecoinPublicKeyHash = 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 dogecoinAmount = 864200L; // 0.00864200 DOGE
|
||||
private static final String SYMBOL = "DOGE";
|
||||
private static final String NAME = "Dogecoin";
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
@Override
|
||||
protected byte[] getPublicKey() {
|
||||
return dogecoinPublicKeyHash;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = DogecoinACCTv3.buildQortalAT(tradeAccount.getAddress(), dogecoinPublicKeyHash, redeemAmount, dogecoinAmount, tradeTimeout);
|
||||
assertNotNull(creationBytes);
|
||||
|
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
@Override
|
||||
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
|
||||
return DogecoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = fundingAmount;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = deployersInitialBalance;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = 0;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected ACCT getInstance() {
|
||||
return DogecoinACCTv3.getInstance();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = DogecoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
|
||||
return DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Instead of sending creator's address to AT, send too-short/invalid message
|
||||
byte[] messageData = new byte[7];
|
||||
RANDOM.nextBytes(messageData);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||
return DogecoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testTradingInfoProcessing() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check hashOfSecretA was extracted correctly
|
||||
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||
|
||||
// Check trade partner Qortal address was extracted correctly
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's dogecoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(dogecoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
|
||||
return DogecoinACCTv3.buildRedeemMessage(secretA, address);
|
||||
}
|
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(AcctMode.OFFERING, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] getCodeBytesHash() {
|
||||
return DogecoinACCTv3.CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
// Check refund
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected String getSymbol() {
|
||||
return SYMBOL;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account
|
||||
messageData = DogecoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REDEEMED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
|
||||
}
|
||||
@Override
|
||||
protected String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageData = DogecoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send incorrect secret to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32];
|
||||
RANDOM.nextBytes(wrongSecret);
|
||||
messageData = DogecoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA);
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should be in TRADING mode
|
||||
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
for (ATData atData : executableAts) {
|
||||
String atAddress = atData.getATAddress();
|
||||
byte[] codeBytes = atData.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
|
||||
atAddress,
|
||||
codeBytes.length,
|
||||
(codeBytes.length != 1 ? "s": ""),
|
||||
HashCode.fromBytes(codeHash)));
|
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, DogecoinACCTv3.CODE_BYTES_HASH))
|
||||
continue;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
|
||||
byte[] creationBytes = DogecoinACCTv3.buildQortalAT(tradeAddress, dogecoinPublicKeyHash, redeemAmount, dogecoinAmount, 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-DOGE cross-chain trade";
|
||||
String description = String.format("Qortal-Dogecoin cross-chain trade");
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-DOGE 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) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
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;
|
||||
}
|
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
int refundTimeout = tradeTimeout / 2 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
}
|
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tmode: %s\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected dogecoin: %s DOGE,\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
atData.getIsFinished(),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund timeout: %d minutes,\n"
|
||||
+ "\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
+ "\tDogecoin P2SH-A nLockTime: %d (%s),\n"
|
||||
+ "\ttrade partner: %s\n"
|
||||
+ "\tpartner's receiving address: %s",
|
||||
tradeData.refundTimeout,
|
||||
tradeData.tradeRefundHeight,
|
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||
tradeData.qortalPartnerAddress,
|
||||
tradeData.qortalPartnerReceivingAddress));
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKeyAccount createTradeAccount(Repository repository) {
|
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,770 +1,60 @@
|
||||
package org.qortal.test.crosschain.litecoinv1;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
|
||||
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.crosschain.ACCT;
|
||||
import org.qortal.crosschain.LitecoinACCTv1;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.test.crosschain.ACCTTests;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
public class LitecoinACCTv1Tests extends Common {
|
||||
public class LitecoinACCTv1Tests extends ACCTTests {
|
||||
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
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
|
||||
private static final String SYMBOL = "LTC";
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
private static final String NAME = "Litecoin";
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
@Override
|
||||
protected byte[] getPublicKey() {
|
||||
return litecoinPublicKeyHash;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
|
||||
assertNotNull(creationBytes);
|
||||
|
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
@Override
|
||||
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
|
||||
return LitecoinACCTv1.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = fundingAmount;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = deployersInitialBalance;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = 0;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected ACCT getInstance() {
|
||||
return LitecoinACCTv1.getInstance();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = LitecoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
|
||||
return LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Instead of sending creator's address to AT, send too-short/invalid message
|
||||
byte[] messageData = new byte[7];
|
||||
RANDOM.nextBytes(messageData);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||
return LitecoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testTradingInfoProcessing() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check hashOfSecretA was extracted correctly
|
||||
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||
|
||||
// Check trade partner Qortal address was extracted correctly
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's Litecoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
|
||||
return LitecoinACCTv1.buildRedeemMessage(secretA, address);
|
||||
}
|
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(AcctMode.OFFERING, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] getCodeBytesHash() {
|
||||
return LitecoinACCTv1.CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
// Check refund
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected String getSymbol() {
|
||||
return SYMBOL;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account
|
||||
messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REDEEMED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
|
||||
}
|
||||
@Override
|
||||
protected String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send incorrect secret to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32];
|
||||
RANDOM.nextBytes(wrongSecret);
|
||||
messageData = LitecoinACCTv1.buildRedeemMessage(wrongSecret, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA);
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should be in TRADING mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
for (ATData atData : executableAts) {
|
||||
String atAddress = atData.getATAddress();
|
||||
byte[] codeBytes = atData.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
|
||||
atAddress,
|
||||
codeBytes.length,
|
||||
(codeBytes.length != 1 ? "s": ""),
|
||||
HashCode.fromBytes(codeHash)));
|
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, LitecoinACCTv1.CODE_BYTES_HASH))
|
||||
continue;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
|
||||
byte[] creationBytes = LitecoinACCTv1.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) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
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;
|
||||
}
|
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
int refundTimeout = tradeTimeout / 2 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
}
|
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tmode: %s\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected Litecoin: %s LTC,\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
atData.getIsFinished(),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund timeout: %d minutes,\n"
|
||||
+ "\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
+ "\tLitecoin P2SH-A nLockTime: %d (%s),\n"
|
||||
+ "\ttrade partner: %s\n"
|
||||
+ "\tpartner's receiving address: %s",
|
||||
tradeData.refundTimeout,
|
||||
tradeData.tradeRefundHeight,
|
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||
tradeData.qortalPartnerAddress,
|
||||
tradeData.qortalPartnerReceivingAddress));
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKeyAccount createTradeAccount(Repository repository) {
|
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,769 +1,58 @@
|
||||
package org.qortal.test.crosschain.litecoinv3;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
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.crosschain.AcctMode;
|
||||
import org.qortal.crosschain.LitecoinACCTv3;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.LitecoinACCTv1;
|
||||
import org.qortal.test.crosschain.ACCTTests;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
public class LitecoinACCTv3Tests extends ACCTTests {
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class LitecoinACCTv3Tests extends Common {
|
||||
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
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
|
||||
private static final String SYMBOL = "LTC";
|
||||
private static final String NAME = "Litecoin";
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
@Override
|
||||
protected byte[] getPublicKey() {
|
||||
return litecoinPublicKeyHash;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = LitecoinACCTv3.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
|
||||
assertNotNull(creationBytes);
|
||||
|
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
@Override
|
||||
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
|
||||
return LitecoinACCTv1.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = fundingAmount;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = deployersInitialBalance;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = 0;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected ACCT getInstance() {
|
||||
return LitecoinACCTv1.getInstance();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = LitecoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// 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);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
|
||||
return LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Instead of sending creator's address to AT, send too-short/invalid message
|
||||
byte[] messageData = new byte[7];
|
||||
RANDOM.nextBytes(messageData);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// 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);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||
return LitecoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testTradingInfoProcessing() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check hashOfSecretA was extracted correctly
|
||||
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||
|
||||
// Check trade partner Qortal address was extracted correctly
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's Litecoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
|
||||
return LitecoinACCTv1.buildRedeemMessage(secretA, address);
|
||||
}
|
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(AcctMode.OFFERING, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] getCodeBytesHash() {
|
||||
return LitecoinACCTv1.CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
// Check refund
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected String getSymbol() {
|
||||
return SYMBOL;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account
|
||||
messageData = LitecoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REDEEMED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
|
||||
}
|
||||
@Override
|
||||
protected String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageData = LitecoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send incorrect secret to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32];
|
||||
RANDOM.nextBytes(wrongSecret);
|
||||
messageData = LitecoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA);
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should be in TRADING mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
for (ATData atData : executableAts) {
|
||||
String atAddress = atData.getATAddress();
|
||||
byte[] codeBytes = atData.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
|
||||
atAddress,
|
||||
codeBytes.length,
|
||||
(codeBytes.length != 1 ? "s": ""),
|
||||
HashCode.fromBytes(codeHash)));
|
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, LitecoinACCTv3.CODE_BYTES_HASH))
|
||||
continue;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
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) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
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;
|
||||
}
|
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
int refundTimeout = tradeTimeout / 2 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
}
|
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tmode: %s\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected Litecoin: %s LTC,\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
atData.getIsFinished(),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund timeout: %d minutes,\n"
|
||||
+ "\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
+ "\tLitecoin P2SH-A nLockTime: %d (%s),\n"
|
||||
+ "\ttrade partner: %s\n"
|
||||
+ "\tpartner's receiving address: %s",
|
||||
tradeData.refundTimeout,
|
||||
tradeData.tradeRefundHeight,
|
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||
tradeData.qortalPartnerAddress,
|
||||
tradeData.qortalPartnerReceivingAddress));
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKeyAccount createTradeAccount(Repository repository) {
|
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,771 +1,58 @@
|
||||
package org.qortal.test.crosschain.piratechainv3;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
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.crosschain.AcctMode;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.PirateChainACCTv3;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.test.crosschain.ACCTTests;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
public class PirateChainACCTv3Tests extends ACCTTests {
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class PirateChainACCTv3Tests extends Common {
|
||||
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final byte[] pirateChainPublicKey = HashCode.fromString("aabb00bb11bb22bb33bb44bb55bb66bb77bb88bb99cc00cc11cc22cc33cc44cc55").asBytes(); // 33 bytes
|
||||
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 arrrAmount = 864200L; // 0.00864200 ARRR
|
||||
private static final String SYMBOL = "ARRR";
|
||||
private static final String NAME = "Pirate Chain";
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
@Override
|
||||
protected byte[] getPublicKey() {
|
||||
return pirateChainPublicKey;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = PirateChainACCTv3.buildQortalAT(tradeAccount.getAddress(), pirateChainPublicKey, redeemAmount, arrrAmount, tradeTimeout);
|
||||
assertNotNull(creationBytes);
|
||||
|
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
@Override
|
||||
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
|
||||
return PirateChainACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = fundingAmount;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = deployersInitialBalance;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = 0;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected ACCT getInstance() {
|
||||
return PirateChainACCTv3.getInstance();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = PirateChainACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
|
||||
return PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Instead of sending creator's address to AT, send too-short/invalid message
|
||||
byte[] messageData = new byte[7];
|
||||
RANDOM.nextBytes(messageData);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||
return PirateChainACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testTradingInfoProcessing() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
System.out.println(String.format("pirateChainPublicKey: %s", HashCode.fromBytes(pirateChainPublicKey)));
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check hashOfSecretA was extracted correctly
|
||||
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||
|
||||
// Check trade partner Qortal address was extracted correctly
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's Litecoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(pirateChainPublicKey, tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
|
||||
return PirateChainACCTv3.buildRedeemMessage(secretA, address);
|
||||
}
|
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(AcctMode.OFFERING, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] getCodeBytesHash() {
|
||||
return PirateChainACCTv3.CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
// Check refund
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected String getSymbol() {
|
||||
return SYMBOL;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account
|
||||
messageData = PirateChainACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REDEEMED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
|
||||
}
|
||||
@Override
|
||||
protected String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageData = PirateChainACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send incorrect secret to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32];
|
||||
RANDOM.nextBytes(wrongSecret);
|
||||
messageData = PirateChainACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA);
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should be in TRADING mode
|
||||
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
for (ATData atData : executableAts) {
|
||||
String atAddress = atData.getATAddress();
|
||||
byte[] codeBytes = atData.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
|
||||
atAddress,
|
||||
codeBytes.length,
|
||||
(codeBytes.length != 1 ? "s": ""),
|
||||
HashCode.fromBytes(codeHash)));
|
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, PirateChainACCTv3.CODE_BYTES_HASH))
|
||||
continue;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
|
||||
byte[] creationBytes = PirateChainACCTv3.buildQortalAT(tradeAddress, pirateChainPublicKey, redeemAmount, arrrAmount, 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-ARRR cross-chain trade";
|
||||
String description = String.format("Qortal-PirateChain cross-chain trade");
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-ARRR 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) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
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;
|
||||
}
|
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
int refundTimeout = tradeTimeout / 2 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
}
|
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tmode: %s\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected ARRR: %s ARRR,\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
atData.getIsFinished(),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund timeout: %d minutes,\n"
|
||||
+ "\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
+ "\tPirate Chain P2SH-A nLockTime: %d (%s),\n"
|
||||
+ "\ttrade partner: %s\n"
|
||||
+ "\tpartner's receiving address: %s",
|
||||
tradeData.refundTimeout,
|
||||
tradeData.tradeRefundHeight,
|
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||
tradeData.qortalPartnerAddress,
|
||||
tradeData.qortalPartnerReceivingAddress));
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKeyAccount createTradeAccount(Repository repository) {
|
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,769 +1,58 @@
|
||||
package org.qortal.test.crosschain.ravencoinv3;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
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.crosschain.AcctMode;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.RavencoinACCTv3;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
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.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.test.crosschain.ACCTTests;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
public class RavencoinACCTv3Tests extends ACCTTests {
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class RavencoinACCTv3Tests extends Common {
|
||||
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final byte[] ravencoinPublicKeyHash = 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 ravencoinAmount = 864200L; // 0.00864200 RVN
|
||||
private static final String SYMBOL = "RVN";
|
||||
private static final String NAME = "Ravencoin";
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
@Override
|
||||
protected byte[] getPublicKey() {
|
||||
return ravencoinPublicKeyHash;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAccount.getAddress(), ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, tradeTimeout);
|
||||
assertNotNull(creationBytes);
|
||||
|
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
@Override
|
||||
protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
|
||||
return RavencoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = fundingAmount;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = deployersInitialBalance;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = 0;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected ACCT getInstance() {
|
||||
return RavencoinACCTv3.getInstance();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = RavencoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
|
||||
return RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Instead of sending creator's address to AT, send too-short/invalid message
|
||||
byte[] messageData = new byte[7];
|
||||
RANDOM.nextBytes(messageData);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||
return RavencoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testTradingInfoProcessing() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check hashOfSecretA was extracted correctly
|
||||
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||
|
||||
// Check trade partner Qortal address was extracted correctly
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's ravencoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(ravencoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected byte[] buildRedeemMessage(byte[] secretA, String address) {
|
||||
return RavencoinACCTv3.buildRedeemMessage(secretA, address);
|
||||
}
|
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(AcctMode.OFFERING, tradeData.mode);
|
||||
}
|
||||
@Override
|
||||
protected byte[] getCodeBytesHash() {
|
||||
return RavencoinACCTv3.CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
// Check refund
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
@Override
|
||||
protected String getSymbol() {
|
||||
return SYMBOL;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account
|
||||
messageData = RavencoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.REDEEMED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
|
||||
}
|
||||
@Override
|
||||
protected String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageData = RavencoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send incorrect secret to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32];
|
||||
RANDOM.nextBytes(wrongSecret);
|
||||
messageData = RavencoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA);
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should be in TRADING mode
|
||||
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
assertEquals(AcctMode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
for (ATData atData : executableAts) {
|
||||
String atAddress = atData.getATAddress();
|
||||
byte[] codeBytes = atData.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
|
||||
atAddress,
|
||||
codeBytes.length,
|
||||
(codeBytes.length != 1 ? "s": ""),
|
||||
HashCode.fromBytes(codeHash)));
|
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, RavencoinACCTv3.CODE_BYTES_HASH))
|
||||
continue;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
|
||||
byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAddress, ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, 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-RVN cross-chain trade";
|
||||
String description = String.format("Qortal-Ravencoin cross-chain trade");
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-RVN 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) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
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;
|
||||
}
|
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
int refundTimeout = tradeTimeout / 2 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
}
|
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tmode: %s\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected ravencoin: %s RVN,\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
atData.getIsFinished(),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund timeout: %d minutes,\n"
|
||||
+ "\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
+ "\tRavencoin P2SH-A nLockTime: %d (%s),\n"
|
||||
+ "\ttrade partner: %s\n"
|
||||
+ "\tpartner's receiving address: %s",
|
||||
tradeData.refundTimeout,
|
||||
tradeData.tradeRefundHeight,
|
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||
tradeData.qortalPartnerAddress,
|
||||
tradeData.qortalPartnerReceivingAddress));
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKeyAccount createTradeAccount(Repository repository) {
|
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
682
src/test/java/org/qortal/test/minting/BatchRewardTests.java
Normal file
682
src/test/java/org/qortal/test/minting/BatchRewardTests.java
Normal file
@@ -0,0 +1,682 @@
|
||||
package org.qortal.test.minting;
|
||||
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.junit.After;
|
||||
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.block.BlockChain;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.test.common.*;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BatchRewardTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useSettings("test-settings-v2-reward-levels.json");
|
||||
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() throws DataException {
|
||||
Common.orphanCheck();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBatchReward() throws DataException, IllegalAccessException {
|
||||
// Set reward batching to every 10 blocks, starting at block 20, looking back the last 3 blocks for online accounts
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 20, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
|
||||
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
|
||||
Long blockReward = BlockUtils.getNextBlockReward(repository);
|
||||
|
||||
// Deploy an AT so we have transaction fees in each block
|
||||
// This also mints block 2
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, Common.getTestAccount(repository, "bob"), AtUtils.buildSimpleAT(), 1_00000000L);
|
||||
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 2);
|
||||
|
||||
long expectedBalance = initialBalances.get("alice").get(Asset.QORT) + blockReward + deployAtTransaction.getTransactionData().getFee();
|
||||
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
|
||||
long aliceCurrentBalance = expectedBalance;
|
||||
|
||||
AccountUtils.assertBlocksMinted(repository, "alice", 1);
|
||||
|
||||
// Mint blocks 3-20
|
||||
Block block;
|
||||
for (int i=3; i<=20; i++) {
|
||||
expectedBalance = aliceCurrentBalance + BlockUtils.getNextBlockReward(repository);
|
||||
block = BlockUtils.mintBlockWithReorgs(repository, 10);
|
||||
expectedBalance += block.getBlockData().getTotalFees();
|
||||
assertFalse(block.isBatchRewardDistributionActive());
|
||||
assertTrue(block.isRewardDistributionBlock());
|
||||
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
|
||||
aliceCurrentBalance = expectedBalance;
|
||||
}
|
||||
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 20);
|
||||
|
||||
AccountUtils.assertBlocksMinted(repository, "alice", 19);
|
||||
|
||||
// Mint blocks 21-29
|
||||
long expectedFees = 0L;
|
||||
for (int i=21; i<=29; i++) {
|
||||
|
||||
// Create payment transaction so that an additional fee is added to the next block
|
||||
Account recipient = AccountUtils.createRandomAccount(repository);
|
||||
TransactionData paymentTransactionData = new PaymentTransactionData(TestTransaction.generateBase(bob), recipient.getAddress(), 100000L);
|
||||
TransactionUtils.signAndImportValid(repository, paymentTransactionData, bob);
|
||||
|
||||
block = BlockUtils.mintBlockWithReorgs(repository, 8);
|
||||
expectedFees += block.getBlockData().getTotalFees();
|
||||
|
||||
// Batch distribution now active
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
|
||||
// It's not a distribution block because we haven't reached the batch size yet
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 29);
|
||||
|
||||
AccountUtils.assertBlocksMinted(repository, "alice", 19);
|
||||
|
||||
// No payouts since block 20 due to batching (to be paid at block 30)
|
||||
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
|
||||
|
||||
// Block reward to be used for next batch payout
|
||||
blockReward = BlockUtils.getNextBlockReward(repository);
|
||||
|
||||
// Mint block 30
|
||||
block = BlockUtils.mintBlockWithReorgs(repository, 9);
|
||||
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 30);
|
||||
|
||||
expectedFees += block.getBlockData().getTotalFees();
|
||||
assertTrue(expectedFees > 0);
|
||||
|
||||
AccountUtils.assertBlocksMinted(repository, "alice", 29);
|
||||
|
||||
// Batch distribution still active
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
|
||||
// It's a distribution block
|
||||
assertTrue(block.isRewardDistributionBlock());
|
||||
|
||||
// Balance should increase by the block reward multiplied by the batch size
|
||||
expectedBalance = aliceCurrentBalance + (blockReward * BlockChain.getInstance().getBlockRewardBatchSize()) + expectedFees;
|
||||
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
|
||||
|
||||
// Mint blocks 31-39
|
||||
for (int i=31; i<=39; i++) {
|
||||
block = BlockUtils.mintBlockWithReorgs(repository, 13);
|
||||
|
||||
// Batch distribution still active
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
|
||||
// It's not a distribution block because we haven't reached the batch size yet
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 39);
|
||||
|
||||
AccountUtils.assertBlocksMinted(repository, "alice", 29);
|
||||
|
||||
// No payouts since block 30 due to batching (to be paid at block 40)
|
||||
AccountUtils.assertBalance(repository, "alice", Asset.QORT, expectedBalance);
|
||||
|
||||
// Batch distribution still active
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
|
||||
// It's not a distribution block
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBatchRewardOnlineAccounts() throws DataException, IllegalAccessException {
|
||||
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
|
||||
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
|
||||
PrivateKeyAccount dilbertSelfShare = Common.getTestAccount(repository, "dilbert-reward-share");
|
||||
|
||||
// Create self shares for bob, chloe and dilbert
|
||||
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
|
||||
|
||||
// Mint blocks 2-6
|
||||
for (int i=2; i<=6; i++) {
|
||||
Block block = BlockUtils.mintBlockWithReorgs(repository, 5);
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
|
||||
// Mint block 7
|
||||
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare);
|
||||
Block block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(2, block7.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 8
|
||||
onlineAccounts = Arrays.asList(aliceSelfShare, chloeSelfShare);
|
||||
Block block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(2, block8.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 9
|
||||
onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, dilbertSelfShare);
|
||||
Block block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(3, block9.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 10
|
||||
Block block10 = BlockUtils.mintBlockWithReorgs(repository, 11);
|
||||
|
||||
// Online accounts should be included from block 8
|
||||
assertEquals(3, block10.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10);
|
||||
|
||||
// It's a distribution block
|
||||
assertTrue(block10.isBatchRewardDistributionBlock());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBatchReward1000Blocks() throws DataException, IllegalAccessException {
|
||||
// Set reward batching to every 1000 blocks, starting at block 1000, looking back the last 25 blocks for online accounts
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 1000, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 1000, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 25, true);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
|
||||
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
|
||||
|
||||
// Create self shares for bob, chloe and dilbert
|
||||
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
|
||||
|
||||
// Mint blocks 2-1000 - these should be regular non-batched reward distribution blocks
|
||||
for (int i=2; i<=1000; i++) {
|
||||
Block block = BlockUtils.mintBlockWithReorgs(repository, 2);
|
||||
assertFalse(block.isBatchRewardDistributionActive());
|
||||
assertTrue(block.isRewardDistributionBlock());
|
||||
assertFalse(block.isBatchRewardDistributionBlock());
|
||||
assertTrue(block.isOnlineAccountsBlock());
|
||||
}
|
||||
|
||||
// Mint blocks 1001-1974 - these should have no online accounts or rewards
|
||||
for (int i=1001; i<=1974; i++) {
|
||||
Block block = BlockUtils.mintBlockWithReorgs(repository, 2);
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
assertFalse(block.isBatchRewardDistributionBlock());
|
||||
assertFalse(block.isOnlineAccountsBlock());
|
||||
assertEquals(0, block.getBlockData().getOnlineAccountsCount());
|
||||
}
|
||||
|
||||
// Mint blocks 1975-1999 - these should have online accounts but no rewards
|
||||
for (int i=1975; i<=1998; i++) {
|
||||
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare);
|
||||
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
assertFalse(block.isBatchRewardDistributionBlock());
|
||||
assertTrue(block.isOnlineAccountsBlock());
|
||||
assertEquals(2, block.getBlockData().getOnlineAccountsCount());
|
||||
}
|
||||
|
||||
// Mint block 1999 - same as above, but with more online accounts
|
||||
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
|
||||
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
assertFalse(block.isBatchRewardDistributionBlock());
|
||||
assertTrue(block.isOnlineAccountsBlock());
|
||||
assertEquals(3, block.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 2000
|
||||
Block block2000 = BlockUtils.mintBlockWithReorgs(repository, 12);
|
||||
|
||||
// Online accounts should be included from block 1999
|
||||
assertEquals(3, block2000.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 2000);
|
||||
|
||||
// It's a distribution block (which is technically also an online accounts block)
|
||||
assertTrue(block2000.isBatchRewardDistributionBlock());
|
||||
assertTrue(block2000.isRewardDistributionBlock());
|
||||
assertTrue(block2000.isBatchRewardDistributionActive());
|
||||
assertTrue(block2000.isOnlineAccountsBlock());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBatchRewardHighestOnlineAccountsCount() throws DataException, IllegalAccessException {
|
||||
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
|
||||
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
|
||||
PrivateKeyAccount dilbertSelfShare = Common.getTestAccount(repository, "dilbert-reward-share");
|
||||
|
||||
// Create self shares for bob, chloe and dilbert
|
||||
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
|
||||
|
||||
// Mint blocks 2-6
|
||||
for (int i=2; i<=6; i++) {
|
||||
Block block = BlockUtils.mintBlockWithReorgs(repository, 3);
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
|
||||
// Capture initial balances now that the online accounts test is ready to begin
|
||||
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
|
||||
|
||||
// Mint block 7
|
||||
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare);
|
||||
Block block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(2, block7.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 8
|
||||
onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
|
||||
Block block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(3, block8.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 9
|
||||
onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, dilbertSelfShare);
|
||||
Block block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(3, block9.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 10
|
||||
Block block10 = BlockUtils.mintBlockWithReorgs(repository, 7);
|
||||
|
||||
// Online accounts should be included from block 8
|
||||
assertEquals(3, block10.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Dilbert's balance should remain the same as he wasn't included in block 8
|
||||
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, initialBalances.get("dilbert").get(Asset.QORT));
|
||||
|
||||
// Alice, Bob, and Chloe's balances should have increased, as they were all included in block 8 (and therefore block 10)
|
||||
AccountUtils.assertBalanceGreaterThan(repository, "alice", Asset.QORT, initialBalances.get("alice").get(Asset.QORT));
|
||||
AccountUtils.assertBalanceGreaterThan(repository, "bob", Asset.QORT, initialBalances.get("bob").get(Asset.QORT));
|
||||
AccountUtils.assertBalanceGreaterThan(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT));
|
||||
|
||||
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10);
|
||||
|
||||
// It's a distribution block
|
||||
assertTrue(block10.isBatchRewardDistributionBlock());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBatchRewardNoOnlineAccounts() throws DataException, IllegalAccessException {
|
||||
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
|
||||
// Mint blocks 2-6 with no online accounts
|
||||
for (int i=2; i<=6; i++) {
|
||||
Block block = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare);
|
||||
assertNotNull("Minted block must not be null", block);
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
|
||||
// Mint block 7 with no online accounts
|
||||
Block block7 = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare);
|
||||
assertNull("Minted block must be null", block7);
|
||||
|
||||
// Mint block 7, this time with an online account
|
||||
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare);
|
||||
block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertNotNull("Minted block must not be null", block7);
|
||||
assertEquals(1, block7.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 8 with no online accounts
|
||||
Block block8 = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare);
|
||||
assertNull("Minted block must be null", block8);
|
||||
|
||||
// Mint block 8, this time with an online account
|
||||
block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertNotNull("Minted block must not be null", block8);
|
||||
assertEquals(1, block8.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 9 with no online accounts
|
||||
Block block9 = BlockMinter.mintTestingBlockUnvalidatedWithoutOnlineAccounts(repository, aliceSelfShare);
|
||||
assertNull("Minted block must be null", block9);
|
||||
|
||||
// Mint block 9, this time with an online account
|
||||
block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertNotNull("Minted block must not be null", block9);
|
||||
assertEquals(1, block9.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 10
|
||||
Block block10 = BlockUtils.mintBlockWithReorgs(repository, 8);
|
||||
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10);
|
||||
|
||||
// It's a distribution block
|
||||
assertTrue(block10.isBatchRewardDistributionBlock());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingOnlineAccountsInDistributionBlock() throws DataException, IllegalAccessException {
|
||||
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
|
||||
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
|
||||
|
||||
// Create self shares for bob, chloe and dilbert
|
||||
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
|
||||
|
||||
// Mint blocks 2-6
|
||||
for (int i=2; i<=6; i++) {
|
||||
Block block = BlockUtils.mintBlockWithReorgs(repository, 9);
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
|
||||
// Mint blocks 7-9
|
||||
for (int i=7; i<=9; i++) {
|
||||
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
|
||||
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(3, block.getBlockData().getOnlineAccountsCount());
|
||||
}
|
||||
|
||||
// Mint block 10
|
||||
Block block10 = Block.mint(repository, repository.getBlockRepository().getLastBlock(), aliceSelfShare);
|
||||
assertNotNull(block10);
|
||||
|
||||
// Remove online accounts (incorrect as there should be 3)
|
||||
block10.getBlockData().setEncodedOnlineAccounts(new byte[0]);
|
||||
|
||||
block10.sign();
|
||||
block10.clearOnlineAccountsValidationCache();
|
||||
|
||||
// Must be invalid because online accounts don't match
|
||||
assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignaturesIncludedInDistributionBlock() throws DataException, IllegalAccessException {
|
||||
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
|
||||
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
|
||||
|
||||
// Create self shares for bob, chloe and dilbert
|
||||
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
|
||||
|
||||
// Mint blocks 2-6
|
||||
for (int i=2; i<=6; i++) {
|
||||
Block block = BlockUtils.mintBlockWithReorgs(repository, 4);
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
|
||||
// Mint blocks 7-9
|
||||
for (int i=7; i<=9; i++) {
|
||||
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
|
||||
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(3, block.getBlockData().getOnlineAccountsCount());
|
||||
}
|
||||
|
||||
// Mint block 10
|
||||
BlockData previousBlock = repository.getBlockRepository().getLastBlock();
|
||||
Block block10 = Block.mint(repository, previousBlock, aliceSelfShare);
|
||||
assertNotNull(block10);
|
||||
|
||||
// Include online accounts signatures
|
||||
block10.getBlockData().setOnlineAccountsSignatures(previousBlock.getOnlineAccountsSignatures());
|
||||
|
||||
block10.sign();
|
||||
block10.clearOnlineAccountsValidationCache();
|
||||
|
||||
// Must be invalid because signatures aren't allowed to be included
|
||||
assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlineAccountsTimestampIncludedInDistributionBlock() throws DataException, IllegalAccessException {
|
||||
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
|
||||
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
|
||||
|
||||
// Create self shares for bob, chloe and dilbert
|
||||
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
|
||||
|
||||
// Mint blocks 2-6
|
||||
for (int i=2; i<=6; i++) {
|
||||
Block block = BlockUtils.mintBlockWithReorgs(repository, 6);
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
|
||||
// Mint blocks 7-9
|
||||
for (int i=7; i<=9; i++) {
|
||||
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
|
||||
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(3, block.getBlockData().getOnlineAccountsCount());
|
||||
}
|
||||
|
||||
// Mint block 10
|
||||
BlockData previousBlock = repository.getBlockRepository().getLastBlock();
|
||||
Block block10 = Block.mint(repository, previousBlock, aliceSelfShare);
|
||||
assertNotNull(block10);
|
||||
|
||||
// Include online accounts timestamp
|
||||
block10.getBlockData().setOnlineAccountsTimestamp(previousBlock.getOnlineAccountsTimestamp());
|
||||
|
||||
block10.sign();
|
||||
block10.clearOnlineAccountsValidationCache();
|
||||
|
||||
// Must be invalid because timestamp isn't allowed to be included
|
||||
assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncorrectOnlineAccountsCountInDistributionBlock() throws DataException, IllegalAccessException {
|
||||
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
|
||||
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
|
||||
|
||||
// Create self shares for bob, chloe and dilbert
|
||||
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
|
||||
|
||||
// Mint blocks 2-6
|
||||
for (int i=2; i<=6; i++) {
|
||||
Block block = BlockUtils.mintBlockWithReorgs(repository, 5);
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
|
||||
// Mint blocks 7-9
|
||||
for (int i=7; i<=9; i++) {
|
||||
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
|
||||
Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(3, block.getBlockData().getOnlineAccountsCount());
|
||||
}
|
||||
|
||||
// Mint block 10
|
||||
BlockData previousBlock = repository.getBlockRepository().getLastBlock();
|
||||
Block block10 = Block.mint(repository, previousBlock, aliceSelfShare);
|
||||
assertNotNull(block10);
|
||||
|
||||
// Update online accounts count so that it is incorrect
|
||||
block10.getBlockData().setOnlineAccountsCount(10);
|
||||
|
||||
block10.sign();
|
||||
block10.clearOnlineAccountsValidationCache();
|
||||
|
||||
// Must be invalid because online accounts count is incorrect
|
||||
assertEquals(Block.ValidationResult.ONLINE_ACCOUNTS_INVALID, block10.isValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBatchRewardBlockSerialization() throws DataException, IllegalAccessException, TransformationException {
|
||||
// Set reward batching to every 10 blocks, starting at block 0, looking back the last 3 blocks for online accounts
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 10, true);
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 3, true);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share");
|
||||
PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share");
|
||||
PrivateKeyAccount dilbertSelfShare = Common.getTestAccount(repository, "dilbert-reward-share");
|
||||
|
||||
// Create self shares for bob, chloe and dilbert
|
||||
AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert));
|
||||
|
||||
// Mint blocks 2-6
|
||||
Block block = null;
|
||||
for (int i=2; i<=6; i++) {
|
||||
block = BlockUtils.mintBlockWithReorgs(repository, 7);
|
||||
assertTrue(block.isBatchRewardDistributionActive());
|
||||
assertFalse(block.isRewardDistributionBlock());
|
||||
}
|
||||
|
||||
// Test serialising and deserializing a block with no online accounts
|
||||
BlockData block6Data = block.getBlockData();
|
||||
byte[] block6Bytes = BlockTransformer.toBytes(block);
|
||||
BlockData block6DataDeserialized = BlockTransformer.fromBytes(block6Bytes).getBlockData();
|
||||
BlockUtils.assertEqual(block6Data, block6DataDeserialized);
|
||||
|
||||
// Capture initial balances now that the online accounts test is ready to begin
|
||||
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT);
|
||||
|
||||
// Mint block 7
|
||||
List<PrivateKeyAccount> onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare);
|
||||
Block block7 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(2, block7.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 8
|
||||
onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare);
|
||||
Block block8 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(3, block8.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 9
|
||||
onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, dilbertSelfShare);
|
||||
Block block9 = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
assertEquals(3, block9.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Mint block 10
|
||||
Block block10 = BlockUtils.mintBlockWithReorgs(repository, 15);
|
||||
|
||||
// Online accounts should be included from block 8
|
||||
assertEquals(3, block10.getBlockData().getOnlineAccountsCount());
|
||||
|
||||
// Dilbert's balance should remain the same as he wasn't included in block 8
|
||||
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, initialBalances.get("dilbert").get(Asset.QORT));
|
||||
|
||||
// Alice, Bob, and Chloe's balances should have increased, as they were all included in block 8 (and therefore block 10)
|
||||
AccountUtils.assertBalanceGreaterThan(repository, "alice", Asset.QORT, initialBalances.get("alice").get(Asset.QORT));
|
||||
AccountUtils.assertBalanceGreaterThan(repository, "bob", Asset.QORT, initialBalances.get("bob").get(Asset.QORT));
|
||||
AccountUtils.assertBalanceGreaterThan(repository, "chloe", Asset.QORT, initialBalances.get("chloe").get(Asset.QORT));
|
||||
|
||||
assertEquals(repository.getBlockRepository().getBlockchainHeight(), 10);
|
||||
|
||||
// It's a distribution block
|
||||
assertTrue(block10.isBatchRewardDistributionBlock());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -444,6 +444,7 @@ public class MiscTests extends Common {
|
||||
// Payment
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
|
||||
|
||||
PaymentTransactionData transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 100000);
|
||||
transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
|
||||
@@ -473,16 +474,16 @@ public class MiscTests extends Common {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
ValidationResult result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be valid", ValidationResult.OK == result);
|
||||
assertEquals("Transaction should be valid", ValidationResult.OK, result);
|
||||
|
||||
// Now try fetching and setting fee manually
|
||||
transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 50000);
|
||||
transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), chloe.getAddress(), 50000);
|
||||
transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
|
||||
assertEquals(300000000L, transactionData.getFee().longValue());
|
||||
transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be valid", ValidationResult.OK == result);
|
||||
assertEquals("Transaction should be valid", ValidationResult.OK, result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 9999999999999,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 0,
|
||||
"blockRewardBatchStartHeight": 999999000,
|
||||
"blockRewardBatchSize": 10,
|
||||
"blockRewardBatchAccountsBlockCount": 3,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"transactionExpiryPeriod": 86400000,
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.001",
|
||||
"unitFees": [
|
||||
{ "timestamp": 0, "fee": "0.001" }
|
||||
],
|
||||
"nameRegistrationUnitFees": [
|
||||
{ "timestamp": 0, "fee": "1.25" }
|
||||
],
|
||||
@@ -24,6 +26,7 @@
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 0,
|
||||
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||
"mempowTransactionUpdatesTimestamp": 1692554400000,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 5.00 },
|
||||
{ "height": 259201, "reward": 4.75 },
|
||||
|
||||
@@ -50,7 +50,7 @@ tx_json=$( cat <<TX_END
|
||||
{
|
||||
"timestamp": ${timestamp},
|
||||
"reference": "${lastref}",
|
||||
"fee": 0.001,
|
||||
"fee": 0.01,
|
||||
"txGroupId": 0,
|
||||
"adminPublicKey": "${pubkey}",
|
||||
"pendingSignature": "${sig}",
|
||||
|
||||
@@ -50,7 +50,7 @@ tx_json=$( cat <<TX_END
|
||||
{
|
||||
"timestamp": ${timestamp},
|
||||
"reference": "${lastref}",
|
||||
"fee": 0.001,
|
||||
"fee": 0.01,
|
||||
"txGroupId": 0,
|
||||
"adminPublicKey": "${pubkey}",
|
||||
"pendingSignature": "${sig}",
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use POSIX;
|
||||
use Getopt::Std;
|
||||
use File::Slurp;
|
||||
|
||||
sub usage() {
|
||||
die("usage: $0 [-p api-port] dev-private-key [short-commit-hash]\n");
|
||||
}
|
||||
|
||||
my %opt;
|
||||
getopts('p:', \%opt);
|
||||
|
||||
usage() if @ARGV < 1 || @ARGV > 2;
|
||||
|
||||
my $port = $opt{p} || 12391;
|
||||
my $privkey = shift @ARGV;
|
||||
my $commit_hash = shift @ARGV;
|
||||
|
||||
my $git_dir = `git rev-parse --show-toplevel`;
|
||||
die("Cannot determine git top level dir\n") unless $git_dir;
|
||||
|
||||
chomp $git_dir;
|
||||
chdir($git_dir) || die("Can't change directory to $git_dir: $!\n");
|
||||
|
||||
open(POM, '<', 'pom.xml') || die ("Can't open 'pom.xml': $!\n");
|
||||
my $project;
|
||||
while (<POM>) {
|
||||
if (m/<artifactId>(\w+)<.artifactId>/o) {
|
||||
$project = $1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
close(POM);
|
||||
|
||||
my $apikey = read_file('apikey.txt');
|
||||
|
||||
# Do we need to determine commit hash?
|
||||
unless ($commit_hash) {
|
||||
# determine git branch
|
||||
my $branch_name = ` git symbolic-ref -q HEAD `;
|
||||
chomp $branch_name;
|
||||
$branch_name =~ s|^refs/heads/||; # ${branch_name##refs/heads/}
|
||||
|
||||
# short-form commit hash on base branch (non-auto-update)
|
||||
$commit_hash ||= `git show --no-patch --format=%h`;
|
||||
die("Can't find commit hash\n") if ! defined $commit_hash;
|
||||
chomp $commit_hash;
|
||||
printf "Commit hash on '%s' branch: %s\n", $branch_name, $commit_hash;
|
||||
} else {
|
||||
printf "Using given commit hash: %s\n", $commit_hash;
|
||||
}
|
||||
|
||||
# build timestamp / commit timestamp on base branch
|
||||
my $timestamp = `git show --no-patch --format=%ct ${commit_hash}`;
|
||||
die("Can't determine commit timestamp\n") if ! defined $timestamp;
|
||||
$timestamp *= 1000; # Convert to milliseconds
|
||||
|
||||
# locate sha256 utility
|
||||
my $SHA256 = `which sha256sum || which sha256`;
|
||||
chomp $SHA256;
|
||||
die("Can't find sha256sum or sha256\n") unless length($SHA256) > 0;
|
||||
|
||||
# SHA256 of actual update file
|
||||
my $sha256 = `git show auto-update-${commit_hash}:${project}.update | ${SHA256} | head -c 64`;
|
||||
die("Can't calculate SHA256 of ${project}.update\n") unless $sha256 =~ m/(\S{64})/;
|
||||
chomp $sha256;
|
||||
|
||||
# long-form commit hash of HEAD on auto-update branch
|
||||
my $update_hash = `git rev-parse refs/heads/auto-update-${commit_hash}`;
|
||||
die("Can't find commit hash for HEAD on auto-update-${commit_hash} branch\n") if ! defined $update_hash;
|
||||
chomp $update_hash;
|
||||
|
||||
printf "Build timestamp (ms): %d / 0x%016x\n", $timestamp, $timestamp;
|
||||
printf "Auto-update commit hash: %s\n", $update_hash;
|
||||
printf "SHA256 of ${project}.update: %s\n", $sha256;
|
||||
|
||||
my $tx_type = 10;
|
||||
my $tx_timestamp = time() * 1000;
|
||||
my $tx_group_id = 1;
|
||||
my $service = 1;
|
||||
printf "\nARBITRARY(%d) transaction with timestamp %d, txGroupID %d and service %d\n", $tx_type, $tx_timestamp, $tx_group_id, $service;
|
||||
|
||||
my $data_hex = sprintf "%016x%s%s", $timestamp, $update_hash, $sha256;
|
||||
printf "\nARBITRARY transaction data payload: %s\n", $data_hex;
|
||||
|
||||
my $n_payments = 0;
|
||||
my $data_type = 1; # RAW_DATA
|
||||
my $data_length = length($data_hex) / 2; # two hex chars per byte
|
||||
my $fee = 0;
|
||||
my $nonce = 0;
|
||||
my $name_length = 0;
|
||||
my $identifier_length = 0;
|
||||
my $method = 0; # PUT
|
||||
my $secret_length = 0;
|
||||
my $compression = 0; # None
|
||||
my $metadata_hash_length = 0;
|
||||
|
||||
die("Something's wrong: data length is not 60 bytes!\n") if $data_length != 60;
|
||||
|
||||
my $pubkey = `curl --silent --url http://localhost:${port}/utils/publickey --data ${privkey}`;
|
||||
die("Can't convert private key to public key:\n$pubkey\n") unless $pubkey =~ m/^\w{44}$/;
|
||||
printf "\nPublic key: %s\n", $pubkey;
|
||||
|
||||
my $pubkey_hex = `curl --silent --url http://localhost:${port}/utils/frombase58 --data ${pubkey}`;
|
||||
die("Can't convert base58 public key to hex:\n$pubkey_hex\n") unless $pubkey_hex =~ m/^[A-Za-z0-9]{64}$/;
|
||||
printf "Public key hex: %s\n", $pubkey_hex;
|
||||
|
||||
my $address = `curl --silent --url http://localhost:${port}/addresses/convert/${pubkey}`;
|
||||
die("Can't convert base58 public key to address:\n$address\n") unless $address =~ m/^\w{33,34}$/;
|
||||
printf "Address: %s\n", $address;
|
||||
|
||||
my $reference = `curl --silent --url http://localhost:${port}/addresses/lastreference/${address}`;
|
||||
die("Can't fetch last reference for $address:\n$reference\n") unless $reference =~ m/^\w{87,88}$/;
|
||||
printf "Last reference: %s\n", $reference;
|
||||
|
||||
my $reference_hex = `curl --silent --url http://localhost:${port}/utils/frombase58 --data ${reference}`;
|
||||
die("Can't convert base58 reference to hex:\n$reference_hex\n") unless $reference_hex =~ m/^[A-Za-z0-9]{128}$/;
|
||||
printf "Last reference hex: %s\n", $reference_hex;
|
||||
|
||||
my $raw_tx_hex = sprintf("%08x%016x%08x%s%s%08x%08x%08x%08x%08x%08x%08x%08x%02x%08x%s%08x%08x%016x", $tx_type, $tx_timestamp, $tx_group_id, $reference_hex, $pubkey_hex, $nonce, $name_length, $identifier_length, $method, $secret_length, $compression, $n_payments, $service, $data_type, $data_length, $data_hex, $data_length, $metadata_hash_length, $fee);
|
||||
printf "\nRaw transaction hex:\n%s\n", $raw_tx_hex;
|
||||
|
||||
my $raw_tx = `curl --silent --url http://localhost:${port}/utils/tobase58/${raw_tx_hex}`;
|
||||
die("Can't convert raw transaction hex to base58:\n$raw_tx\n") unless $raw_tx =~ m/^\w{300,320}$/; # Roughly 305 to 320 base58 chars
|
||||
printf "\nRaw transaction (base58):\n%s\n", $raw_tx;
|
||||
|
||||
my $computed_tx = `curl --silent -X POST --url http://localhost:${port}/arbitrary/compute -H "X-API-KEY: ${apikey}" -d "${raw_tx}"`;
|
||||
die("Can't compute nonce for transaction:\n$computed_tx\n") unless $computed_tx =~ m/^\w{300,320}$/; # Roughly 300 to 320 base58 chars
|
||||
printf "\nRaw computed transaction (base58):\n%s\n", $computed_tx;
|
||||
|
||||
my $sign_data = qq|' { "privateKey": "${privkey}", "transactionBytes": "${computed_tx}" } '|;
|
||||
my $signed_tx = `curl --silent -H "accept: text/plain" -H "Content-Type: application/json" --url http://localhost:${port}/transactions/sign --data ${sign_data}`;
|
||||
die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{390,410}$/; # +90ish longer than $raw_tx
|
||||
printf "\nSigned transaction:\n%s\n", $signed_tx;
|
||||
|
||||
# Check we can actually fetch update
|
||||
my $origin = `git remote get-url origin`;
|
||||
die("Unable to get github url for 'origin'?\n") unless $origin && $origin =~ m/:(.*)\.git$/;
|
||||
my $repo = $1;
|
||||
my $update_url = "https://github.com/${repo}/raw/${update_hash}/${project}.update";
|
||||
|
||||
my $fetch_result = `curl --silent -o /dev/null --location --range 0-1 --head --write-out '%{http_code}' --url ${update_url}`;
|
||||
die("\nUnable to fetch update from ${update_url}\n") if $fetch_result ne '200';
|
||||
printf "\nUpdate fetchable from ${update_url}\n";
|
||||
|
||||
# Flush STDOUT after every output
|
||||
$| = 1;
|
||||
print "\n";
|
||||
for (my $delay = 5; $delay > 0; --$delay) {
|
||||
printf "\rSubmitting transaction in %d second%s... CTRL-C to abort ", $delay, ($delay != 1 ? 's' : '');
|
||||
sleep 1;
|
||||
}
|
||||
|
||||
printf "\rSubmitting transaction NOW... \n";
|
||||
my $result = `curl --silent --url http://localhost:${port}/transactions/process --data ${signed_tx}`;
|
||||
chomp $result;
|
||||
die("Transaction wasn't accepted:\n$result\n") unless $result eq 'true';
|
||||
|
||||
my $decoded_tx = `curl --silent -H "Content-Type: application/json" --url http://localhost:${port}/transactions/decode --data ${signed_tx}`;
|
||||
printf "\nTransaction accepted:\n$decoded_tx\n";
|
||||
@@ -4,6 +4,7 @@ use strict;
|
||||
use warnings;
|
||||
use POSIX;
|
||||
use Getopt::Std;
|
||||
use File::Slurp;
|
||||
|
||||
sub usage() {
|
||||
die("usage: $0 [-p api-port] dev-private-key [short-commit-hash]\n");
|
||||
@@ -34,6 +35,8 @@ while (<POM>) {
|
||||
}
|
||||
close(POM);
|
||||
|
||||
my $apikey = read_file('apikey.txt');
|
||||
|
||||
# Do we need to determine commit hash?
|
||||
unless ($commit_hash) {
|
||||
# determine git branch
|
||||
@@ -84,9 +87,16 @@ my $data_hex = sprintf "%016x%s%s", $timestamp, $update_hash, $sha256;
|
||||
printf "\nARBITRARY transaction data payload: %s\n", $data_hex;
|
||||
|
||||
my $n_payments = 0;
|
||||
my $is_raw = 1; # RAW_DATA
|
||||
my $data_type = 1; # RAW_DATA
|
||||
my $data_length = length($data_hex) / 2; # two hex chars per byte
|
||||
my $fee = 0.001 * 1e8;
|
||||
my $fee = 0.01 * 1e8;
|
||||
my $nonce = 0;
|
||||
my $name_length = 0;
|
||||
my $identifier_length = 0;
|
||||
my $method = 0; # PUT
|
||||
my $secret_length = 0;
|
||||
my $compression = 0; # None
|
||||
my $metadata_hash_length = 0;
|
||||
|
||||
die("Something's wrong: data length is not 60 bytes!\n") if $data_length != 60;
|
||||
|
||||
@@ -110,16 +120,16 @@ my $reference_hex = `curl --silent --url http://localhost:${port}/utils/frombase
|
||||
die("Can't convert base58 reference to hex:\n$reference_hex\n") unless $reference_hex =~ m/^[A-Za-z0-9]{128}$/;
|
||||
printf "Last reference hex: %s\n", $reference_hex;
|
||||
|
||||
my $raw_tx_hex = sprintf("%08x%016x%08x%s%s%08x%08x%02x%08x%s%016x", $tx_type, $tx_timestamp, $tx_group_id, $reference_hex, $pubkey_hex, $n_payments, $service, $is_raw, $data_length, $data_hex, $fee);
|
||||
my $raw_tx_hex = sprintf("%08x%016x%08x%s%s%08x%08x%08x%08x%08x%08x%08x%08x%02x%08x%s%08x%08x%016x", $tx_type, $tx_timestamp, $tx_group_id, $reference_hex, $pubkey_hex, $nonce, $name_length, $identifier_length, $method, $secret_length, $compression, $n_payments, $service, $data_type, $data_length, $data_hex, $data_length, $metadata_hash_length, $fee);
|
||||
printf "\nRaw transaction hex:\n%s\n", $raw_tx_hex;
|
||||
|
||||
my $raw_tx = `curl --silent --url http://localhost:${port}/utils/tobase58/${raw_tx_hex}`;
|
||||
die("Can't convert raw transaction hex to base58:\n$raw_tx\n") unless $raw_tx =~ m/^\w{255,265}$/; # Roughly 255 to 265 base58 chars
|
||||
die("Can't convert raw transaction hex to base58:\n$raw_tx\n") unless $raw_tx =~ m/^\w{300,320}$/; # Roughly 305 to 320 base58 chars
|
||||
printf "\nRaw transaction (base58):\n%s\n", $raw_tx;
|
||||
|
||||
my $sign_data = qq|' { "privateKey": "${privkey}", "transactionBytes": "${raw_tx}" } '|;
|
||||
my $signed_tx = `curl --silent -H "accept: text/plain" -H "Content-Type: application/json" --url http://localhost:${port}/transactions/sign --data ${sign_data}`;
|
||||
die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{345,355}$/; # +90ish longer than $raw_tx
|
||||
die("Can't sign raw transaction:\n$signed_tx\n") unless $signed_tx =~ m/^\w{390,410}$/; # +90ish longer than $raw_tx
|
||||
printf "\nSigned transaction:\n%s\n", $signed_tx;
|
||||
|
||||
# Check we can actually fetch update
|
||||
|
||||
122
tools/qdn
122
tools/qdn
@@ -5,10 +5,10 @@ host="localhost"
|
||||
port=12391
|
||||
|
||||
if [ -z "$*" ]; then
|
||||
echo "Usage:"
|
||||
echo "Usage:"
|
||||
echo
|
||||
echo "Host/update data:"
|
||||
echo "qdn POST [service] [name] PATH [dirpath] <identifier>"
|
||||
echo "qdn POST [service] [name] PATH [dirpath] <identifier> <title> <description> <tags=tag1,tag2,tag3> <category> <fee> <preview (true or false)>"
|
||||
echo "qdn POST [service] [name] STRING [data-string] <identifier>"
|
||||
echo
|
||||
echo "Fetch data:"
|
||||
@@ -22,6 +22,21 @@ if [ -z "$*" ]; then
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
# Default ports for Qortal
|
||||
mainnet_port=12391
|
||||
testnet_port=62391
|
||||
|
||||
# Check if the '-t' operator is passed, if so change to utilizing testnet.
|
||||
if [[ "$1" == "-t" ]]; then
|
||||
# Use testnet port
|
||||
port=$testnet_port
|
||||
shift
|
||||
else
|
||||
# Use mainnet port
|
||||
port=$mainnet_port
|
||||
fi
|
||||
|
||||
script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
if [ -f "apikey.txt" ]; then
|
||||
@@ -37,32 +52,46 @@ service=$2
|
||||
name=$3
|
||||
|
||||
if [ -z "${method}" ]; then
|
||||
echo "Error: missing method"; exit
|
||||
echo "Error: missing method"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${service}" ]; then
|
||||
echo "Error: missing service"; exit
|
||||
echo "Error: missing service"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${name}" ]; then
|
||||
echo "Error: missing name"; exit
|
||||
echo "Error: missing name"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [[ "${method}" == "POST" ]]; then
|
||||
type=$4
|
||||
data=$5
|
||||
identifier=$6
|
||||
title=$7
|
||||
description=$8
|
||||
tags=$9
|
||||
category=${10}
|
||||
fee=${11}
|
||||
preview=${12}
|
||||
|
||||
|
||||
|
||||
if [ -z "${data}" ]; then
|
||||
if [[ "${type}" == "PATH" ]]; then
|
||||
echo "Error: missing directory"; exit
|
||||
echo "Error: missing directory - please use a path to a directory with a SINGLE file wishing to be published"
|
||||
exit 1
|
||||
elif [[ "${type}" == "STRING" ]]; then
|
||||
echo "Error: missing data string"; exit
|
||||
echo "Error: missing data string - please input the data string you wish to publish"
|
||||
exit 1
|
||||
else
|
||||
echo "Error: unrecognized type"; exit
|
||||
echo "Error: unrecognized type"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ -z "${QORTAL_PRIVKEY}" ]; then
|
||||
echo "Error: missing private key. Set it by running: export QORTAL_PRIVKEY=privkeyhere"; exit
|
||||
echo "Error: missing private key. Set it by running: export QORTAL_PRIVKEY=privkeyhere"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${identifier}" ]; then
|
||||
@@ -75,30 +104,88 @@ if [[ "${method}" == "POST" ]]; then
|
||||
elif [[ "${type}" == "STRING" ]]; then
|
||||
type_component="/string"
|
||||
fi
|
||||
|
||||
# Create tags component in URL, comma-separated list of tags, will be added to the tags call.
|
||||
tags_component=""
|
||||
if [ -n "${tags}" ]; then
|
||||
IFS=',' read -ra tag_array <<< "${tags}"
|
||||
for tag in "${tag_array[@]}"; do
|
||||
tags_component+="&tags=${tag}"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z ${tags_component} ]; then
|
||||
tags_component=""
|
||||
echo "nothing in tags, using empty tags"
|
||||
fi
|
||||
|
||||
#Create category component with pre-defined list of categories. Error if category is specified but not in list.
|
||||
allowed_categories=("ART" "AUTOMOTIVE" "BEAUTY" "BOOKS" "BUSINESS" "COMMUNICATIONS" "CRYPTOCURRENCY" "CULTURE" "DATING" "DESIGN" "ENTERTAINMENT" "EVENTS" "FAITH" "FASHION" "FINANCE" "FOOD" "GAMING" "GEOGRAPHY" "HEALTH" "HISTORY" "HOME" "KNOWLEDGE" "LANGUAGE" "LIFESTYLE" "MANUFACTURING" "MAPS" "MUSIC" "NEWS" "OTHER" "PETS" "PHILOSOPHY" "PHOTOGRAPHY" "POLITICS" "PRODUCE" "PRODUCTIVITY" "PSYCHOLOGY" "QORTAL" "SCIENCE" "SELF_CARE" "SELF_SUFFICIENCY" "SHOPPING" "SOCIAL" "SOFTWARE" "SPIRITUALITY" "SPORTS" "STORYTELLING" "TECHNOLOGY" "TOOLS" "TRAVEL" "UNCATEGORIZED" "VIDEO" "WEATHER")
|
||||
|
||||
if [[ -n "$category" && ! " ${allowed_categories[@]} " =~ " $category " ]]; then
|
||||
echo "Error: Invalid category. Allowed categories are: ${allowed_categories[*]} be sure to place your overall script inputs in the correct order"
|
||||
exit 1
|
||||
elif [ -z "$category" ]; then
|
||||
category=""
|
||||
echo "No category is being set"
|
||||
fi
|
||||
|
||||
if [ -n "$fee" ]; then
|
||||
if [[ "$fee" == "1" || "$fee" == ".01" ]]; then
|
||||
fee="1000000"
|
||||
elif [ -z "$fee" ]; then
|
||||
fee=""
|
||||
else
|
||||
echo "Error: Invalid fee value. Expected '1', '.01' or no input."
|
||||
exit 1
|
||||
fi
|
||||
final_fee="${fee}"
|
||||
fi
|
||||
|
||||
|
||||
# check that preview is true/false
|
||||
if [[ -n "$preview" && ! ( "$preview" == "true" || "$preview" == "false" ) ]]; then
|
||||
echo "Error: Invalid preview value. Expected 'true' or 'false'. Please retry with boolean as preview entry."
|
||||
exit 1
|
||||
elif [ -z "$preview" ]; then
|
||||
preview=""
|
||||
fi
|
||||
|
||||
# Build the API URL
|
||||
api_url="http://${host}:${port}/arbitrary/${service}/${name}/${identifier}${type_component}"
|
||||
api_url+="?title=${title}&description=${description}&tags=${tags_component}&category=${category}&fee=${final_fee}&preview=${preview}"
|
||||
|
||||
|
||||
echo "Creating transaction - this can take a while..."
|
||||
tx_data=$(curl --silent --insecure -X ${method} "http://${host}:${port}/arbitrary/${service}/${name}/${identifier}${type_component}" -H "X-API-KEY: ${apikey}" -d "${data}")
|
||||
tx_data=$(curl --silent --insecure -X ${method} "${api_url}" -H "accept: text/plain" -H "X-API-KEY: ${apikey}" -H "Content-Type: text/plain" -d "${data}")
|
||||
|
||||
if [[ "${tx_data}" == *"error"* || "${tx_data}" == *"ERROR"* ]]; then
|
||||
echo "${tx_data}"; exit
|
||||
echo "Error creating transaction: ${tx_data}"
|
||||
exit 1
|
||||
elif [ -z "${tx_data}" ]; then
|
||||
echo "Error: no transaction data returned"; exit
|
||||
echo "Error: no transaction data returned"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Computing nonce..."
|
||||
computed_tx_data=$(curl --silent --insecure -X POST "http://${host}:${port}/arbitrary/compute" -H "Content-Type: application/json" -H "X-API-KEY: ${apikey}" -d "${tx_data}")
|
||||
|
||||
if [[ "${computed_tx_data}" == *"error"* || "${computed_tx_data}" == *"ERROR"* ]]; then
|
||||
echo "${computed_tx_data}"; exit
|
||||
echo "Error computing nonce: ${computed_tx_data}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Signing..."
|
||||
signed_tx_data=$(curl --silent --insecure -X POST "http://${host}:${port}/transactions/sign" -H "Content-Type: application/json" -d "{\"privateKey\":\"${QORTAL_PRIVKEY}\",\"transactionBytes\":\"${computed_tx_data}\"}")
|
||||
|
||||
if [[ "${signed_tx_data}" == *"error"* || "${signed_tx_data}" == *"ERROR"* ]]; then
|
||||
echo "${signed_tx_data}"; exit
|
||||
echo "Error signing transaction: ${signed_tx_data}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Broadcasting..."
|
||||
success=$(curl --silent --insecure -X POST "http://${host}:${port}/transactions/process" -H "Content-Type: text/plain" -d "${signed_tx_data}")
|
||||
|
||||
if [[ "${success}" == "true" ]]; then
|
||||
echo "Transaction broadcast successfully"
|
||||
else
|
||||
@@ -131,9 +218,10 @@ elif [[ "${method}" == "GET" ]]; then
|
||||
echo "Empty response from ${host}:${port}"
|
||||
fi
|
||||
if [[ "${response}" == *"error"* || "${response}" == *"ERROR"* ]]; then
|
||||
echo "${response}"; exit
|
||||
echo "${response}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${response}"
|
||||
|
||||
fi
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ our %b58 = map { $b58[$_] => $_ } 0 .. 57;
|
||||
our %reverseb58 = reverse %b58;
|
||||
|
||||
our $BASE_URL = $ENV{BASE_URL} || ($opt{t} ? 'http://localhost:62391' : 'http://localhost:12391');
|
||||
our $DEFAULT_FEE = 0.001;
|
||||
our $DEFAULT_FEE = 0.01;
|
||||
|
||||
our %TRANSACTION_TYPES = (
|
||||
payment => {
|
||||
@@ -92,7 +92,7 @@ our %TRANSACTION_TYPES = (
|
||||
},
|
||||
remove_group_admin => {
|
||||
url => 'groups/removeadmin',
|
||||
required => [qw(groupId txGroupId member)],
|
||||
required => [qw(groupId txGroupId admin)],
|
||||
key_name => 'ownerPublicKey',
|
||||
},
|
||||
group_approval => {
|
||||
|
||||
Reference in New Issue
Block a user