mirror of
https://github.com/Qortal/qortal.git
synced 2025-05-03 08:17:50 +00:00
265 lines
8.8 KiB
Java
265 lines
8.8 KiB
Java
package qora.transaction;
|
|
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.math.BigDecimal;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
|
|
import org.ciyam.at.MachineState;
|
|
|
|
import com.google.common.base.Utf8;
|
|
|
|
import data.assets.AssetData;
|
|
import data.transaction.DeployATTransactionData;
|
|
import data.transaction.TransactionData;
|
|
import qora.account.Account;
|
|
import qora.assets.Asset;
|
|
import qora.at.AT;
|
|
import qora.block.BlockChain;
|
|
import qora.crypto.Crypto;
|
|
import repository.DataException;
|
|
import repository.Repository;
|
|
import transform.Transformer;
|
|
|
|
public class DeployATTransaction extends Transaction {
|
|
|
|
// Properties
|
|
private DeployATTransactionData deployATTransactionData;
|
|
|
|
// Other useful constants
|
|
public static final int MAX_NAME_SIZE = 200;
|
|
public static final int MAX_DESCRIPTION_SIZE = 2000;
|
|
public static final int MAX_AT_TYPE_SIZE = 200;
|
|
public static final int MAX_TAGS_SIZE = 200;
|
|
public static final int MAX_CREATION_BYTES_SIZE = 100_000;
|
|
|
|
// Constructors
|
|
|
|
public DeployATTransaction(Repository repository, TransactionData transactionData) {
|
|
super(repository, transactionData);
|
|
|
|
this.deployATTransactionData = (DeployATTransactionData) this.transactionData;
|
|
}
|
|
|
|
// More information
|
|
|
|
@Override
|
|
public List<Account> getRecipientAccounts() throws DataException {
|
|
return new ArrayList<Account>();
|
|
}
|
|
|
|
@Override
|
|
public boolean isInvolved(Account account) throws DataException {
|
|
String address = account.getAddress();
|
|
|
|
if (address.equals(this.getCreator().getAddress()))
|
|
return true;
|
|
|
|
if (address.equals(this.getATAccount().getAddress()))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public BigDecimal getAmount(Account account) throws DataException {
|
|
String address = account.getAddress();
|
|
BigDecimal amount = BigDecimal.ZERO.setScale(8);
|
|
|
|
if (address.equals(this.getCreator().getAddress()))
|
|
amount = amount.subtract(this.deployATTransactionData.getAmount()).subtract(this.transactionData.getFee());
|
|
|
|
if (address.equals(this.getATAccount().getAddress()))
|
|
amount = amount.add(this.deployATTransactionData.getAmount());
|
|
|
|
return amount;
|
|
}
|
|
|
|
/** Returns AT version from the header bytes */
|
|
private short getVersion() {
|
|
byte[] creationBytes = deployATTransactionData.getCreationBytes();
|
|
short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
|
|
return version;
|
|
}
|
|
|
|
/** Make sure deployATTransactionData has an ATAddress */
|
|
private void ensureATAddress() throws DataException {
|
|
if (this.deployATTransactionData.getATAddress() != null)
|
|
return;
|
|
|
|
int blockHeight = this.getHeight();
|
|
if (blockHeight == 0)
|
|
blockHeight = this.repository.getBlockRepository().getBlockchainHeight() + 1;
|
|
|
|
try {
|
|
byte[] name = this.deployATTransactionData.getName().getBytes("UTF-8");
|
|
byte[] description = this.deployATTransactionData.getDescription().replaceAll("\\s", "").getBytes("UTF-8");
|
|
byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey();
|
|
byte[] creationBytes = this.deployATTransactionData.getCreationBytes();
|
|
|
|
ByteBuffer byteBuffer = ByteBuffer
|
|
.allocate(name.length + description.length + creatorPublicKey.length + creationBytes.length + Transformer.INT_LENGTH);
|
|
|
|
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
byteBuffer.put(name);
|
|
byteBuffer.put(description);
|
|
byteBuffer.put(creatorPublicKey);
|
|
byteBuffer.put(creationBytes);
|
|
byteBuffer.putInt(blockHeight);
|
|
|
|
String atAddress = Crypto.toATAddress(byteBuffer.array());
|
|
|
|
this.deployATTransactionData.setATAddress(atAddress);
|
|
} catch (UnsupportedEncodingException e) {
|
|
throw new DataException("Unable to generate AT account from Deploy AT transaction data", e);
|
|
}
|
|
}
|
|
|
|
// Navigation
|
|
|
|
public Account getATAccount() throws DataException {
|
|
ensureATAddress();
|
|
|
|
return new Account(this.repository, this.deployATTransactionData.getATAddress());
|
|
}
|
|
|
|
// Processing
|
|
|
|
@Override
|
|
public ValidationResult isValid() throws DataException {
|
|
if (this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getATReleaseHeight())
|
|
return ValidationResult.NOT_YET_RELEASED;
|
|
|
|
// Check name size bounds
|
|
int nameLength = Utf8.encodedLength(deployATTransactionData.getName());
|
|
if (nameLength < 1 || nameLength > MAX_NAME_SIZE)
|
|
return ValidationResult.INVALID_NAME_LENGTH;
|
|
|
|
// Check description size bounds
|
|
int descriptionlength = Utf8.encodedLength(deployATTransactionData.getDescription());
|
|
if (descriptionlength < 1 || descriptionlength > MAX_DESCRIPTION_SIZE)
|
|
return ValidationResult.INVALID_DESCRIPTION_LENGTH;
|
|
|
|
// Check AT-type size bounds
|
|
int ATTypeLength = Utf8.encodedLength(deployATTransactionData.getATType());
|
|
if (ATTypeLength < 1 || ATTypeLength > MAX_AT_TYPE_SIZE)
|
|
return ValidationResult.INVALID_AT_TYPE_LENGTH;
|
|
|
|
// Check tags size bounds
|
|
int tagsLength = Utf8.encodedLength(deployATTransactionData.getTags());
|
|
if (tagsLength < 1 || tagsLength > MAX_TAGS_SIZE)
|
|
return ValidationResult.INVALID_TAGS_LENGTH;
|
|
|
|
// Check amount is positive
|
|
if (deployATTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0)
|
|
return ValidationResult.NEGATIVE_AMOUNT;
|
|
|
|
long assetId = deployATTransactionData.getAssetId();
|
|
AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId);
|
|
// Check asset even exists
|
|
if (assetData == null)
|
|
return ValidationResult.ASSET_DOES_NOT_EXIST;
|
|
|
|
// Check asset amount is integer if asset is not divisible
|
|
if (!assetData.getIsDivisible() && deployATTransactionData.getAmount().stripTrailingZeros().scale() > 0)
|
|
return ValidationResult.INVALID_AMOUNT;
|
|
|
|
// Check fee is positive
|
|
if (deployATTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0)
|
|
return ValidationResult.NEGATIVE_FEE;
|
|
|
|
// Check reference is correct
|
|
Account creator = getCreator();
|
|
|
|
if (!Arrays.equals(creator.getLastReference(), deployATTransactionData.getReference()))
|
|
return ValidationResult.INVALID_REFERENCE;
|
|
|
|
// Check creator has enough funds
|
|
if (assetId == Asset.QORA) {
|
|
// Simple case: amount and fee both in Qora
|
|
BigDecimal minimumBalance = deployATTransactionData.getFee().add(deployATTransactionData.getAmount());
|
|
|
|
if (creator.getConfirmedBalance(Asset.QORA).compareTo(minimumBalance) < 0)
|
|
return ValidationResult.NO_BALANCE;
|
|
} else {
|
|
if (creator.getConfirmedBalance(Asset.QORA).compareTo(deployATTransactionData.getFee()) < 0)
|
|
return ValidationResult.NO_BALANCE;
|
|
|
|
if (creator.getConfirmedBalance(assetId).compareTo(deployATTransactionData.getAmount()) < 0)
|
|
return ValidationResult.NO_BALANCE;
|
|
}
|
|
|
|
// Check creation bytes are valid (for v2+)
|
|
if (this.getVersion() >= 2) {
|
|
// Do actual validation
|
|
try {
|
|
new MachineState(deployATTransactionData.getCreationBytes());
|
|
} catch (IllegalArgumentException e) {
|
|
// Not valid
|
|
return ValidationResult.INVALID_CREATION_BYTES;
|
|
}
|
|
} else {
|
|
// Skip validation for old, dead ATs
|
|
}
|
|
|
|
return ValidationResult.OK;
|
|
}
|
|
|
|
@Override
|
|
public void process() throws DataException {
|
|
ensureATAddress();
|
|
|
|
// Deploy AT, saving into repository
|
|
AT at = new AT(this.repository, this.deployATTransactionData);
|
|
at.deploy();
|
|
|
|
// Save this transaction itself
|
|
this.repository.getTransactionRepository().save(this.transactionData);
|
|
|
|
long assetId = deployATTransactionData.getAssetId();
|
|
|
|
// Update creator's balance
|
|
Account creator = getCreator();
|
|
creator.setConfirmedBalance(assetId, creator.getConfirmedBalance(assetId).subtract(deployATTransactionData.getAmount()));
|
|
creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(deployATTransactionData.getFee()));
|
|
|
|
// Update creator's reference
|
|
creator.setLastReference(deployATTransactionData.getSignature());
|
|
|
|
// Update AT's reference, which also creates AT account
|
|
Account atAccount = this.getATAccount();
|
|
atAccount.setLastReference(deployATTransactionData.getSignature());
|
|
|
|
// Update AT's balance
|
|
atAccount.setConfirmedBalance(assetId, deployATTransactionData.getAmount());
|
|
}
|
|
|
|
@Override
|
|
public void orphan() throws DataException {
|
|
// Delete AT from repository
|
|
AT at = new AT(this.repository, this.deployATTransactionData);
|
|
at.undeploy();
|
|
|
|
// Delete this transaction itself
|
|
this.repository.getTransactionRepository().delete(deployATTransactionData);
|
|
|
|
long assetId = deployATTransactionData.getAssetId();
|
|
|
|
// Update creator's balance
|
|
Account creator = getCreator();
|
|
creator.setConfirmedBalance(assetId, creator.getConfirmedBalance(assetId).add(deployATTransactionData.getAmount()));
|
|
creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(deployATTransactionData.getFee()));
|
|
|
|
// Update creator's reference
|
|
creator.setLastReference(deployATTransactionData.getReference());
|
|
|
|
// Delete AT's account (and hence its balance)
|
|
this.repository.getAccountRepository().delete(this.deployATTransactionData.getATAddress());
|
|
}
|
|
|
|
}
|