/******************************************************************************
* Copyright © 2013-2016 The Nxt Core Developers. *
* *
* See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at *
* the top-level directory of this distribution for the individual copyright *
* holder information and the developer policies on copyright and licensing. *
* *
* Unless otherwise agreed in a custom licensing agreement, no part of the *
* Nxt software, including this file, may be copied, modified, propagated, *
* or distributed except according to the terms contained in the LICENSE.txt *
* file. *
* *
* Removal or modification of this copyright notice is prohibited. *
* *
******************************************************************************/
package nxt;
import nxt.Account.ControlType;
import nxt.AccountLedger.LedgerEvent;
import nxt.Attachment.AbstractAttachment;
import nxt.NxtException.ValidationException;
import nxt.VoteWeighting.VotingModel;
import nxt.util.Convert;
import org.json.simple.JSONObject;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public abstract class TransactionType {
private static final byte TYPE_PAYMENT = 0;
private static final byte TYPE_MESSAGING = 1;
private static final byte TYPE_COLORED_COINS = 2;
private static final byte TYPE_DIGITAL_GOODS = 3;
private static final byte TYPE_ACCOUNT_CONTROL = 4;
static final byte TYPE_MONETARY_SYSTEM = 5;
private static final byte TYPE_DATA = 6;
static final byte TYPE_SHUFFLING = 7;
private static final byte SUBTYPE_PAYMENT_ORDINARY_PAYMENT = 0;
private static final byte SUBTYPE_MESSAGING_ARBITRARY_MESSAGE = 0;
private static final byte SUBTYPE_MESSAGING_ALIAS_ASSIGNMENT = 1;
private static final byte SUBTYPE_MESSAGING_POLL_CREATION = 2;
private static final byte SUBTYPE_MESSAGING_VOTE_CASTING = 3;
private static final byte SUBTYPE_MESSAGING_HUB_ANNOUNCEMENT = 4;
private static final byte SUBTYPE_MESSAGING_ACCOUNT_INFO = 5;
private static final byte SUBTYPE_MESSAGING_ALIAS_SELL = 6;
private static final byte SUBTYPE_MESSAGING_ALIAS_BUY = 7;
private static final byte SUBTYPE_MESSAGING_ALIAS_DELETE = 8;
private static final byte SUBTYPE_MESSAGING_PHASING_VOTE_CASTING = 9;
private static final byte SUBTYPE_MESSAGING_ACCOUNT_PROPERTY = 10;
private static final byte SUBTYPE_MESSAGING_ACCOUNT_PROPERTY_DELETE = 11;
private static final byte SUBTYPE_COLORED_COINS_ASSET_ISSUANCE = 0;
private static final byte SUBTYPE_COLORED_COINS_ASSET_TRANSFER = 1;
private static final byte SUBTYPE_COLORED_COINS_ASK_ORDER_PLACEMENT = 2;
private static final byte SUBTYPE_COLORED_COINS_BID_ORDER_PLACEMENT = 3;
private static final byte SUBTYPE_COLORED_COINS_ASK_ORDER_CANCELLATION = 4;
private static final byte SUBTYPE_COLORED_COINS_BID_ORDER_CANCELLATION = 5;
private static final byte SUBTYPE_COLORED_COINS_DIVIDEND_PAYMENT = 6;
private static final byte SUBTYPE_COLORED_COINS_ASSET_DELETE = 7;
private static final byte SUBTYPE_DIGITAL_GOODS_LISTING = 0;
private static final byte SUBTYPE_DIGITAL_GOODS_DELISTING = 1;
private static final byte SUBTYPE_DIGITAL_GOODS_PRICE_CHANGE = 2;
private static final byte SUBTYPE_DIGITAL_GOODS_QUANTITY_CHANGE = 3;
private static final byte SUBTYPE_DIGITAL_GOODS_PURCHASE = 4;
private static final byte SUBTYPE_DIGITAL_GOODS_DELIVERY = 5;
private static final byte SUBTYPE_DIGITAL_GOODS_FEEDBACK = 6;
private static final byte SUBTYPE_DIGITAL_GOODS_REFUND = 7;
private static final byte SUBTYPE_ACCOUNT_CONTROL_EFFECTIVE_BALANCE_LEASING = 0;
private static final byte SUBTYPE_ACCOUNT_CONTROL_PHASING_ONLY = 1;
private static final byte SUBTYPE_DATA_TAGGED_DATA_UPLOAD = 0;
private static final byte SUBTYPE_DATA_TAGGED_DATA_EXTEND = 1;
public static TransactionType findTransactionType(byte type, byte subtype) {
switch (type) {
case TYPE_PAYMENT:
switch (subtype) {
case SUBTYPE_PAYMENT_ORDINARY_PAYMENT:
return Payment.ORDINARY;
default:
return null;
}
case TYPE_MESSAGING:
switch (subtype) {
case SUBTYPE_MESSAGING_ARBITRARY_MESSAGE:
return Messaging.ARBITRARY_MESSAGE;
case SUBTYPE_MESSAGING_ALIAS_ASSIGNMENT:
return Messaging.ALIAS_ASSIGNMENT;
case SUBTYPE_MESSAGING_POLL_CREATION:
return Messaging.POLL_CREATION;
case SUBTYPE_MESSAGING_VOTE_CASTING:
return Messaging.VOTE_CASTING;
case SUBTYPE_MESSAGING_HUB_ANNOUNCEMENT:
return Messaging.HUB_ANNOUNCEMENT;
case SUBTYPE_MESSAGING_ACCOUNT_INFO:
return Messaging.ACCOUNT_INFO;
case SUBTYPE_MESSAGING_ALIAS_SELL:
return Messaging.ALIAS_SELL;
case SUBTYPE_MESSAGING_ALIAS_BUY:
return Messaging.ALIAS_BUY;
case SUBTYPE_MESSAGING_ALIAS_DELETE:
return Messaging.ALIAS_DELETE;
case SUBTYPE_MESSAGING_PHASING_VOTE_CASTING:
return Messaging.PHASING_VOTE_CASTING;
case SUBTYPE_MESSAGING_ACCOUNT_PROPERTY:
return Messaging.ACCOUNT_PROPERTY;
case SUBTYPE_MESSAGING_ACCOUNT_PROPERTY_DELETE:
return Messaging.ACCOUNT_PROPERTY_DELETE;
default:
return null;
}
case TYPE_COLORED_COINS:
switch (subtype) {
case SUBTYPE_COLORED_COINS_ASSET_ISSUANCE:
return ColoredCoins.ASSET_ISSUANCE;
case SUBTYPE_COLORED_COINS_ASSET_TRANSFER:
return ColoredCoins.ASSET_TRANSFER;
case SUBTYPE_COLORED_COINS_ASK_ORDER_PLACEMENT:
return ColoredCoins.ASK_ORDER_PLACEMENT;
case SUBTYPE_COLORED_COINS_BID_ORDER_PLACEMENT:
return ColoredCoins.BID_ORDER_PLACEMENT;
case SUBTYPE_COLORED_COINS_ASK_ORDER_CANCELLATION:
return ColoredCoins.ASK_ORDER_CANCELLATION;
case SUBTYPE_COLORED_COINS_BID_ORDER_CANCELLATION:
return ColoredCoins.BID_ORDER_CANCELLATION;
case SUBTYPE_COLORED_COINS_DIVIDEND_PAYMENT:
return ColoredCoins.DIVIDEND_PAYMENT;
case SUBTYPE_COLORED_COINS_ASSET_DELETE:
return ColoredCoins.ASSET_DELETE;
default:
return null;
}
case TYPE_DIGITAL_GOODS:
switch (subtype) {
case SUBTYPE_DIGITAL_GOODS_LISTING:
return DigitalGoods.LISTING;
case SUBTYPE_DIGITAL_GOODS_DELISTING:
return DigitalGoods.DELISTING;
case SUBTYPE_DIGITAL_GOODS_PRICE_CHANGE:
return DigitalGoods.PRICE_CHANGE;
case SUBTYPE_DIGITAL_GOODS_QUANTITY_CHANGE:
return DigitalGoods.QUANTITY_CHANGE;
case SUBTYPE_DIGITAL_GOODS_PURCHASE:
return DigitalGoods.PURCHASE;
case SUBTYPE_DIGITAL_GOODS_DELIVERY:
return DigitalGoods.DELIVERY;
case SUBTYPE_DIGITAL_GOODS_FEEDBACK:
return DigitalGoods.FEEDBACK;
case SUBTYPE_DIGITAL_GOODS_REFUND:
return DigitalGoods.REFUND;
default:
return null;
}
case TYPE_ACCOUNT_CONTROL:
switch (subtype) {
case SUBTYPE_ACCOUNT_CONTROL_EFFECTIVE_BALANCE_LEASING:
return TransactionType.AccountControl.EFFECTIVE_BALANCE_LEASING;
case SUBTYPE_ACCOUNT_CONTROL_PHASING_ONLY:
return TransactionType.AccountControl.SET_PHASING_ONLY;
default:
return null;
}
case TYPE_MONETARY_SYSTEM:
return MonetarySystem.findTransactionType(subtype);
case TYPE_DATA:
switch (subtype) {
case SUBTYPE_DATA_TAGGED_DATA_UPLOAD:
return Data.TAGGED_DATA_UPLOAD;
case SUBTYPE_DATA_TAGGED_DATA_EXTEND:
return Data.TAGGED_DATA_EXTEND;
default:
return null;
}
case TYPE_SHUFFLING:
return ShufflingTransaction.findTransactionType(subtype);
default:
return null;
}
}
TransactionType() {}
public abstract byte getType();
public abstract byte getSubtype();
public abstract LedgerEvent getLedgerEvent();
abstract Attachment.AbstractAttachment parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException;
abstract Attachment.AbstractAttachment parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException;
abstract void validateAttachment(Transaction transaction) throws NxtException.ValidationException;
// return false iff double spending
final boolean applyUnconfirmed(TransactionImpl transaction, Account senderAccount) {
long amountNQT = transaction.getAmountNQT();
long feeNQT = transaction.getFeeNQT();
if (transaction.referencedTransactionFullHash() != null
&& transaction.getTimestamp() > Constants.REFERENCED_TRANSACTION_FULL_HASH_BLOCK_TIMESTAMP) {
feeNQT = Math.addExact(feeNQT, Constants.UNCONFIRMED_POOL_DEPOSIT_NQT);
}
long totalAmountNQT = Math.addExact(amountNQT, feeNQT);
if (senderAccount.getUnconfirmedBalanceNQT() < totalAmountNQT
&& !(transaction.getTimestamp() == 0 && Arrays.equals(senderAccount.getPublicKey(), Genesis.CREATOR_PUBLIC_KEY))) {
return false;
}
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), -amountNQT, -feeNQT);
if (!applyAttachmentUnconfirmed(transaction, senderAccount)) {
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), amountNQT, feeNQT);
return false;
}
return true;
}
abstract boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount);
final void apply(TransactionImpl transaction, Account senderAccount, Account recipientAccount) {
long amount = transaction.getAmountNQT();
long transactionId = transaction.getId();
if (!transaction.attachmentIsPhased()) {
senderAccount.addToBalanceNQT(getLedgerEvent(), transactionId, -amount, -transaction.getFeeNQT());
} else {
senderAccount.addToBalanceNQT(getLedgerEvent(), transactionId, -amount);
}
if (recipientAccount != null) {
recipientAccount.addToBalanceAndUnconfirmedBalanceNQT(getLedgerEvent(), transactionId, amount);
}
applyAttachment(transaction, senderAccount, recipientAccount);
}
abstract void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount);
final void undoUnconfirmed(TransactionImpl transaction, Account senderAccount) {
undoAttachmentUnconfirmed(transaction, senderAccount);
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(),
transaction.getAmountNQT(), transaction.getFeeNQT());
if (transaction.referencedTransactionFullHash() != null
&& transaction.getTimestamp() > Constants.REFERENCED_TRANSACTION_FULL_HASH_BLOCK_TIMESTAMP) {
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), 0,
Constants.UNCONFIRMED_POOL_DEPOSIT_NQT);
}
}
abstract void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount);
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
return false;
}
// isBlockDuplicate and isDuplicate share the same duplicates map, but isBlockDuplicate check is done first
boolean isBlockDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
return false;
}
boolean isUnconfirmedDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
return false;
}
static boolean isDuplicate(TransactionType uniqueType, String key, Map<TransactionType, Map<String, Integer>> duplicates, boolean exclusive) {
return isDuplicate(uniqueType, key, duplicates, exclusive ? 0 : Integer.MAX_VALUE);
}
static boolean isDuplicate(TransactionType uniqueType, String key, Map<TransactionType, Map<String, Integer>> duplicates, int maxCount) {
Map<String,Integer> typeDuplicates = duplicates.get(uniqueType);
if (typeDuplicates == null) {
typeDuplicates = new HashMap<>();
duplicates.put(uniqueType, typeDuplicates);
}
Integer currentCount = typeDuplicates.get(key);
if (currentCount == null) {
typeDuplicates.put(key, maxCount > 0 ? 1 : 0);
return false;
}
if (currentCount == 0) {
return true;
}
if (currentCount < maxCount) {
typeDuplicates.put(key, currentCount + 1);
return false;
}
return true;
}
boolean isPruned(long transactionId) {
return false;
}
public abstract boolean canHaveRecipient();
public boolean mustHaveRecipient() {
return canHaveRecipient();
}
public abstract boolean isPhasingSafe();
public boolean isPhasable() {
return true;
}
Fee getBaselineFee(Transaction transaction) {
return Fee.DEFAULT_FEE;
}
Fee getNextFee(Transaction transaction) {
return getBaselineFee(transaction);
}
int getBaselineFeeHeight() {
return 1;
}
int getNextFeeHeight() {
return Integer.MAX_VALUE;
}
long[] getBackFees(Transaction transaction) {
return Convert.EMPTY_LONG;
}
public abstract String getName();
@Override
public final String toString() {
return getName() + " type: " + getType() + ", subtype: " + getSubtype();
}
public static abstract class Payment extends TransactionType {
private Payment() {
}
@Override
public final byte getType() {
return TransactionType.TYPE_PAYMENT;
}
@Override
final boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
return true;
}
@Override
final void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
if (recipientAccount == null) {
Account.getAccount(Genesis.CREATOR_ID).addToBalanceAndUnconfirmedBalanceNQT(getLedgerEvent(),
transaction.getId(), transaction.getAmountNQT());
}
}
@Override
final void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
}
@Override
public final boolean canHaveRecipient() {
return true;
}
@Override
public final boolean isPhasingSafe() {
return true;
}
public static final TransactionType ORDINARY = new Payment() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_PAYMENT_ORDINARY_PAYMENT;
}
@Override
public final LedgerEvent getLedgerEvent() {
return LedgerEvent.ORDINARY_PAYMENT;
}
@Override
public String getName() {
return "OrdinaryPayment";
}
@Override
Attachment.EmptyAttachment parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return Attachment.ORDINARY_PAYMENT;
}
@Override
Attachment.EmptyAttachment parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return Attachment.ORDINARY_PAYMENT;
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
if (transaction.getAmountNQT() <= 0 || transaction.getAmountNQT() >= Constants.MAX_BALANCE_NQT) {
throw new NxtException.NotValidException("Invalid ordinary payment");
}
}
};
}
public static abstract class Messaging extends TransactionType {
private Messaging() {
}
@Override
public final byte getType() {
return TransactionType.TYPE_MESSAGING;
}
@Override
final boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
return true;
}
@Override
final void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
}
public final static TransactionType ARBITRARY_MESSAGE = new Messaging() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_ARBITRARY_MESSAGE;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ARBITRARY_MESSAGE;
}
@Override
public String getName() {
return "ArbitraryMessage";
}
@Override
Attachment.EmptyAttachment parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return Attachment.ARBITRARY_MESSAGE;
}
@Override
Attachment.EmptyAttachment parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return Attachment.ARBITRARY_MESSAGE;
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment attachment = transaction.getAttachment();
if (transaction.getAmountNQT() != 0) {
throw new NxtException.NotValidException("Invalid arbitrary message: " + attachment.getJSONObject());
}
if (transaction.getRecipientId() == Genesis.CREATOR_ID && Nxt.getBlockchain().getHeight() > Constants.MONETARY_SYSTEM_BLOCK) {
throw new NxtException.NotCurrentlyValidException("Sending messages to Genesis not allowed.");
}
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean mustHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public static final TransactionType ALIAS_ASSIGNMENT = new Messaging() {
private final Fee ALIAS_FEE = new Fee.SizeBasedFee(2 * Constants.ONE_NXT, 2 * Constants.ONE_NXT, 32) {
@Override
public int getSize(TransactionImpl transaction, Appendix appendage) {
Attachment.MessagingAliasAssignment attachment = (Attachment.MessagingAliasAssignment) transaction.getAttachment();
return attachment.getAliasName().length() + attachment.getAliasURI().length();
}
};
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_ALIAS_ASSIGNMENT;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ALIAS_ASSIGNMENT;
}
@Override
public String getName() {
return "AliasAssignment";
}
@Override
Fee getNextFee(Transaction transaction) {
return ALIAS_FEE;
}
@Override
int getNextFeeHeight() {
return Constants.SHUFFLING_BLOCK;
}
@Override
Attachment.MessagingAliasAssignment parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingAliasAssignment(buffer, transactionVersion);
}
@Override
Attachment.MessagingAliasAssignment parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingAliasAssignment(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.MessagingAliasAssignment attachment = (Attachment.MessagingAliasAssignment) transaction.getAttachment();
Alias.addOrUpdateAlias(transaction, attachment);
}
@Override
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
Attachment.MessagingAliasAssignment attachment = (Attachment.MessagingAliasAssignment) transaction.getAttachment();
return isDuplicate(Messaging.ALIAS_ASSIGNMENT, attachment.getAliasName().toLowerCase(), duplicates, true);
}
@Override
boolean isBlockDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
return Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK
&& Alias.getAlias(((Attachment.MessagingAliasAssignment) transaction.getAttachment()).getAliasName()) == null
&& isDuplicate(Messaging.ALIAS_ASSIGNMENT, "", duplicates, true);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.MessagingAliasAssignment attachment = (Attachment.MessagingAliasAssignment) transaction.getAttachment();
if (attachment.getAliasName().length() == 0
|| attachment.getAliasName().length() > Constants.MAX_ALIAS_LENGTH
|| attachment.getAliasURI().length() > Constants.MAX_ALIAS_URI_LENGTH) {
throw new NxtException.NotValidException("Invalid alias assignment: " + attachment.getJSONObject());
}
String normalizedAlias = attachment.getAliasName().toLowerCase();
for (int i = 0; i < normalizedAlias.length(); i++) {
if (Constants.ALPHABET.indexOf(normalizedAlias.charAt(i)) < 0) {
throw new NxtException.NotValidException("Invalid alias name: " + normalizedAlias);
}
}
Alias alias = Alias.getAlias(normalizedAlias);
if (alias != null && alias.getAccountId() != transaction.getSenderId()) {
throw new NxtException.NotCurrentlyValidException("Alias already owned by another account: " + normalizedAlias);
}
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public static final TransactionType ALIAS_SELL = new Messaging() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_ALIAS_SELL;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ALIAS_SELL;
}
@Override
public String getName() {
return "AliasSell";
}
@Override
Attachment.MessagingAliasSell parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingAliasSell(buffer, transactionVersion);
}
@Override
Attachment.MessagingAliasSell parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingAliasSell(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.MessagingAliasSell attachment = (Attachment.MessagingAliasSell) transaction.getAttachment();
Alias.sellAlias(transaction, attachment);
}
@Override
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
Attachment.MessagingAliasSell attachment = (Attachment.MessagingAliasSell) transaction.getAttachment();
// not a bug, uniqueness is based on Messaging.ALIAS_ASSIGNMENT
return isDuplicate(Messaging.ALIAS_ASSIGNMENT, attachment.getAliasName().toLowerCase(), duplicates, true);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
if (transaction.getAmountNQT() != 0) {
throw new NxtException.NotValidException("Invalid sell alias transaction: " +
transaction.getJSONObject());
}
final Attachment.MessagingAliasSell attachment =
(Attachment.MessagingAliasSell) transaction.getAttachment();
final String aliasName = attachment.getAliasName();
if (aliasName == null || aliasName.length() == 0) {
throw new NxtException.NotValidException("Missing alias name");
}
long priceNQT = attachment.getPriceNQT();
if (priceNQT < 0 || priceNQT > Constants.MAX_BALANCE_NQT) {
throw new NxtException.NotValidException("Invalid alias sell price: " + priceNQT);
}
if (priceNQT == 0) {
if (Genesis.CREATOR_ID == transaction.getRecipientId()) {
throw new NxtException.NotValidException("Transferring aliases to Genesis account not allowed");
} else if (transaction.getRecipientId() == 0) {
throw new NxtException.NotValidException("Missing alias transfer recipient");
}
}
final Alias alias = Alias.getAlias(aliasName);
if (alias == null) {
throw new NxtException.NotCurrentlyValidException("No such alias: " + aliasName);
} else if (alias.getAccountId() != transaction.getSenderId()) {
throw new NxtException.NotCurrentlyValidException("Alias doesn't belong to sender: " + aliasName);
}
if (transaction.getRecipientId() == Genesis.CREATOR_ID) {
throw new NxtException.NotCurrentlyValidException("Selling alias to Genesis not allowed");
}
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean mustHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public static final TransactionType ALIAS_BUY = new Messaging() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_ALIAS_BUY;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ALIAS_BUY;
}
@Override
public String getName() {
return "AliasBuy";
}
@Override
Attachment.MessagingAliasBuy parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingAliasBuy(buffer, transactionVersion);
}
@Override
Attachment.MessagingAliasBuy parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingAliasBuy(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
final Attachment.MessagingAliasBuy attachment =
(Attachment.MessagingAliasBuy) transaction.getAttachment();
final String aliasName = attachment.getAliasName();
Alias.changeOwner(transaction.getSenderId(), aliasName);
}
@Override
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
Attachment.MessagingAliasBuy attachment = (Attachment.MessagingAliasBuy) transaction.getAttachment();
// not a bug, uniqueness is based on Messaging.ALIAS_ASSIGNMENT
return isDuplicate(Messaging.ALIAS_ASSIGNMENT, attachment.getAliasName().toLowerCase(), duplicates, true);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
final Attachment.MessagingAliasBuy attachment =
(Attachment.MessagingAliasBuy) transaction.getAttachment();
final String aliasName = attachment.getAliasName();
final Alias alias = Alias.getAlias(aliasName);
if (alias == null) {
throw new NxtException.NotCurrentlyValidException("No such alias: " + aliasName);
} else if (alias.getAccountId() != transaction.getRecipientId()) {
throw new NxtException.NotCurrentlyValidException("Alias is owned by account other than recipient: "
+ Long.toUnsignedString(alias.getAccountId()));
}
Alias.Offer offer = Alias.getOffer(alias);
if (offer == null) {
throw new NxtException.NotCurrentlyValidException("Alias is not for sale: " + aliasName);
}
if (transaction.getAmountNQT() < offer.getPriceNQT()) {
String msg = "Price is too low for: " + aliasName + " ("
+ transaction.getAmountNQT() + " < " + offer.getPriceNQT() + ")";
throw new NxtException.NotCurrentlyValidException(msg);
}
if (offer.getBuyerId() != 0 && offer.getBuyerId() != transaction.getSenderId()) {
throw new NxtException.NotCurrentlyValidException("Wrong buyer for " + aliasName + ": "
+ Long.toUnsignedString(transaction.getSenderId()) + " expected: "
+ Long.toUnsignedString(offer.getBuyerId()));
}
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public static final TransactionType ALIAS_DELETE = new Messaging() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_ALIAS_DELETE;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ALIAS_DELETE;
}
@Override
public String getName() {
return "AliasDelete";
}
@Override
Attachment.MessagingAliasDelete parseAttachment(final ByteBuffer buffer, final byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingAliasDelete(buffer, transactionVersion);
}
@Override
Attachment.MessagingAliasDelete parseAttachment(final JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingAliasDelete(attachmentData);
}
@Override
void applyAttachment(final Transaction transaction, final Account senderAccount, final Account recipientAccount) {
final Attachment.MessagingAliasDelete attachment =
(Attachment.MessagingAliasDelete) transaction.getAttachment();
Alias.deleteAlias(attachment.getAliasName());
}
@Override
boolean isDuplicate(final Transaction transaction, final Map<TransactionType, Map<String, Integer>> duplicates) {
Attachment.MessagingAliasDelete attachment = (Attachment.MessagingAliasDelete) transaction.getAttachment();
// not a bug, uniqueness is based on Messaging.ALIAS_ASSIGNMENT
return isDuplicate(Messaging.ALIAS_ASSIGNMENT, attachment.getAliasName().toLowerCase(), duplicates, true);
}
@Override
void validateAttachment(final Transaction transaction) throws NxtException.ValidationException {
final Attachment.MessagingAliasDelete attachment =
(Attachment.MessagingAliasDelete) transaction.getAttachment();
final String aliasName = attachment.getAliasName();
if (aliasName == null || aliasName.length() == 0) {
throw new NxtException.NotValidException("Missing alias name");
}
final Alias alias = Alias.getAlias(aliasName);
if (alias == null) {
throw new NxtException.NotCurrentlyValidException("No such alias: " + aliasName);
} else if (alias.getAccountId() != transaction.getSenderId()) {
throw new NxtException.NotCurrentlyValidException("Alias doesn't belong to sender: " + aliasName);
}
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public final static TransactionType POLL_CREATION = new Messaging() {
private final Fee POLL_FEE = (transaction, appendage) -> {
int numOptions = ((Attachment.MessagingPollCreation)appendage).getPollOptions().length;
return numOptions <= 20 ? 10 * Constants.ONE_NXT : (10 + numOptions - 20) * Constants.ONE_NXT;
};
private final Fee POLL_OPTIONS_FEE = new Fee.SizeBasedFee(10 * Constants.ONE_NXT, Constants.ONE_NXT, 1) {
@Override
public int getSize(TransactionImpl transaction, Appendix appendage) {
int numOptions = ((Attachment.MessagingPollCreation)appendage).getPollOptions().length;
return numOptions <= 19 ? 0 : numOptions - 19;
}
};
private final Fee POLL_SIZE_FEE = new Fee.SizeBasedFee(0, 2 * Constants.ONE_NXT, 32) {
@Override
public int getSize(TransactionImpl transaction, Appendix appendage) {
Attachment.MessagingPollCreation attachment = (Attachment.MessagingPollCreation)appendage;
int size = attachment.getPollName().length() + attachment.getPollDescription().length();
for (String option : ((Attachment.MessagingPollCreation)appendage).getPollOptions()) {
size += option.length();
}
return size <= 288 ? 0 : size - 288;
}
};
private final Fee POLL_FEE_2 = (transaction, appendage) ->
POLL_OPTIONS_FEE.getFee(transaction, appendage) + POLL_SIZE_FEE.getFee(transaction, appendage);
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_POLL_CREATION;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.POLL_CREATION;
}
@Override
public String getName() {
return "PollCreation";
}
@Override
Fee getBaselineFee(Transaction transaction) {
return POLL_FEE;
}
@Override
Fee getNextFee(Transaction transaction) {
return POLL_FEE_2;
}
@Override
int getNextFeeHeight() {
return Constants.SHUFFLING_BLOCK;
}
@Override
Attachment.MessagingPollCreation parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingPollCreation(buffer, transactionVersion);
}
@Override
Attachment.MessagingPollCreation parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingPollCreation(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.MessagingPollCreation attachment = (Attachment.MessagingPollCreation) transaction.getAttachment();
Poll.addPoll(transaction, attachment);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.MessagingPollCreation attachment = (Attachment.MessagingPollCreation) transaction.getAttachment();
int optionsCount = attachment.getPollOptions().length;
if (attachment.getPollName().length() > Constants.MAX_POLL_NAME_LENGTH
|| attachment.getPollName().isEmpty()
|| attachment.getPollDescription().length() > Constants.MAX_POLL_DESCRIPTION_LENGTH
|| optionsCount > Constants.MAX_POLL_OPTION_COUNT
|| optionsCount == 0) {
throw new NxtException.NotValidException("Invalid poll attachment: " + attachment.getJSONObject());
}
if (attachment.getMinNumberOfOptions() < 1
|| attachment.getMinNumberOfOptions() > optionsCount) {
throw new NxtException.NotValidException("Invalid min number of options: " + attachment.getJSONObject());
}
if (attachment.getMaxNumberOfOptions() < 1
|| attachment.getMaxNumberOfOptions() < attachment.getMinNumberOfOptions()
|| attachment.getMaxNumberOfOptions() > optionsCount) {
throw new NxtException.NotValidException("Invalid max number of options: " + attachment.getJSONObject());
}
for (int i = 0; i < optionsCount; i++) {
if (attachment.getPollOptions()[i].length() > Constants.MAX_POLL_OPTION_LENGTH
|| attachment.getPollOptions()[i].isEmpty()) {
throw new NxtException.NotValidException("Invalid poll options length: " + attachment.getJSONObject());
}
}
if (attachment.getMinRangeValue() < Constants.MIN_VOTE_VALUE || attachment.getMaxRangeValue() > Constants.MAX_VOTE_VALUE
|| (Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK && attachment.getMaxRangeValue() < attachment.getMinRangeValue())){
throw new NxtException.NotValidException("Invalid range: " + attachment.getJSONObject());
}
if (attachment.getFinishHeight() <= attachment.getFinishValidationHeight(transaction) + 1
|| attachment.getFinishHeight() >= attachment.getFinishValidationHeight(transaction) + Constants.MAX_POLL_DURATION) {
throw new NxtException.NotCurrentlyValidException("Invalid finishing height" + attachment.getJSONObject());
}
if (! attachment.getVoteWeighting().acceptsVotes() || attachment.getVoteWeighting().getVotingModel() == VoteWeighting.VotingModel.HASH) {
throw new NxtException.NotValidException("VotingModel " + attachment.getVoteWeighting().getVotingModel() + " not valid for regular polls");
}
attachment.getVoteWeighting().validate();
}
@Override
boolean isBlockDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
return Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK
&& isDuplicate(Messaging.POLL_CREATION, getName(), duplicates, true);
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public final static TransactionType VOTE_CASTING = new Messaging() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_VOTE_CASTING;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.VOTE_CASTING;
}
@Override
public String getName() {
return "VoteCasting";
}
@Override
Attachment.MessagingVoteCasting parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingVoteCasting(buffer, transactionVersion);
}
@Override
Attachment.MessagingVoteCasting parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingVoteCasting(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.MessagingVoteCasting attachment = (Attachment.MessagingVoteCasting) transaction.getAttachment();
Vote.addVote(transaction, attachment);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.MessagingVoteCasting attachment = (Attachment.MessagingVoteCasting) transaction.getAttachment();
if (attachment.getPollId() == 0 || attachment.getPollVote() == null
|| attachment.getPollVote().length > Constants.MAX_POLL_OPTION_COUNT) {
throw new NxtException.NotValidException("Invalid vote casting attachment: " + attachment.getJSONObject());
}
long pollId = attachment.getPollId();
Poll poll = Poll.getPoll(pollId);
if (poll == null) {
throw new NxtException.NotCurrentlyValidException("Invalid poll: " + Long.toUnsignedString(attachment.getPollId()));
}
if (Vote.getVote(pollId, transaction.getSenderId()) != null) {
throw new NxtException.NotCurrentlyValidException("Double voting attempt");
}
if (poll.getFinishHeight() <= attachment.getFinishValidationHeight(transaction)) {
throw new NxtException.NotCurrentlyValidException("Voting for this poll finishes at " + poll.getFinishHeight());
}
byte[] votes = attachment.getPollVote();
int positiveCount = 0;
for (byte vote : votes) {
if (vote != Constants.NO_VOTE_VALUE && (vote < poll.getMinRangeValue() || vote > poll.getMaxRangeValue())) {
throw new NxtException.NotValidException(String.format("Invalid vote %d, vote must be between %d and %d",
vote, poll.getMinRangeValue(), poll.getMaxRangeValue()));
}
if (vote != Constants.NO_VOTE_VALUE) {
positiveCount++;
}
}
if (positiveCount < poll.getMinNumberOfOptions() || positiveCount > poll.getMaxNumberOfOptions()) {
throw new NxtException.NotValidException(String.format("Invalid num of choices %d, number of choices must be between %d and %d",
positiveCount, poll.getMinNumberOfOptions(), poll.getMaxNumberOfOptions()));
}
}
@Override
boolean isDuplicate(final Transaction transaction, final Map<TransactionType, Map<String, Integer>> duplicates) {
Attachment.MessagingVoteCasting attachment = (Attachment.MessagingVoteCasting) transaction.getAttachment();
String key = Long.toUnsignedString(attachment.getPollId()) + ":" + Long.toUnsignedString(transaction.getSenderId());
return isDuplicate(Messaging.VOTE_CASTING, key, duplicates, true);
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public static final TransactionType PHASING_VOTE_CASTING = new Messaging() {
private final Fee PHASING_VOTE_FEE = (transaction, appendage) -> {
Attachment.MessagingPhasingVoteCasting attachment = (Attachment.MessagingPhasingVoteCasting) transaction.getAttachment();
return attachment.getTransactionFullHashes().size() * Constants.ONE_NXT;
};
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_PHASING_VOTE_CASTING;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.PHASING_VOTE_CASTING;
}
@Override
public String getName() {
return "PhasingVoteCasting";
}
@Override
Fee getBaselineFee(Transaction transaction) {
return PHASING_VOTE_FEE;
}
@Override
Attachment.MessagingPhasingVoteCasting parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingPhasingVoteCasting(buffer, transactionVersion);
}
@Override
Attachment.MessagingPhasingVoteCasting parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingPhasingVoteCasting(attachmentData);
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.MessagingPhasingVoteCasting attachment = (Attachment.MessagingPhasingVoteCasting) transaction.getAttachment();
byte[] revealedSecret = attachment.getRevealedSecret();
if (revealedSecret.length > Constants.MAX_PHASING_REVEALED_SECRET_LENGTH) {
throw new NxtException.NotValidException("Invalid revealed secret length " + revealedSecret.length);
}
byte[] hashedSecret = null;
byte algorithm = 0;
List<byte[]> hashes = attachment.getTransactionFullHashes();
if (hashes.size() > Constants.MAX_PHASING_VOTE_TRANSACTIONS) {
throw new NxtException.NotValidException("No more than " + Constants.MAX_PHASING_VOTE_TRANSACTIONS + " votes allowed for two-phased multi-voting");
}
long voterId = transaction.getSenderId();
for (byte[] hash : hashes) {
long phasedTransactionId = Convert.fullHashToId(hash);
if (phasedTransactionId == 0) {
throw new NxtException.NotValidException("Invalid phased transactionFullHash " + Convert.toHexString(hash));
}
PhasingPoll poll = PhasingPoll.getPoll(phasedTransactionId);
if (poll == null) {
throw new NxtException.NotCurrentlyValidException("Invalid phased transaction " + Long.toUnsignedString(phasedTransactionId)
+ ", or phasing is finished");
}
if (! poll.getVoteWeighting().acceptsVotes()) {
throw new NxtException.NotValidException("This phased transaction does not require or accept voting");
}
long[] whitelist = poll.getWhitelist();
if (whitelist.length > 0 && Arrays.binarySearch(whitelist, voterId) < 0) {
throw new NxtException.NotValidException("Voter is not in the phased transaction whitelist");
}
if (revealedSecret.length > 0) {
if (poll.getVoteWeighting().getVotingModel() != VoteWeighting.VotingModel.HASH) {
throw new NxtException.NotValidException("Phased transaction " + Long.toUnsignedString(phasedTransactionId) + " does not accept by-hash voting");
}
if (hashedSecret != null && !Arrays.equals(poll.getHashedSecret(), hashedSecret)) {
throw new NxtException.NotValidException("Phased transaction " + Long.toUnsignedString(phasedTransactionId) + " is using a different hashedSecret");
}
if (algorithm != 0 && algorithm != poll.getAlgorithm()) {
throw new NxtException.NotValidException("Phased transaction " + Long.toUnsignedString(phasedTransactionId) + " is using a different hashedSecretAlgorithm");
}
if (hashedSecret == null && ! poll.verifySecret(revealedSecret)) {
throw new NxtException.NotValidException("Revealed secret does not match phased transaction hashed secret");
}
hashedSecret = poll.getHashedSecret();
algorithm = poll.getAlgorithm();
} else if (poll.getVoteWeighting().getVotingModel() == VoteWeighting.VotingModel.HASH) {
throw new NxtException.NotValidException("Phased transaction " + Long.toUnsignedString(phasedTransactionId) + " requires revealed secret for approval");
}
if (!Arrays.equals(poll.getFullHash(), hash)) {
throw new NxtException.NotCurrentlyValidException("Phased transaction hash does not match hash in voting transaction");
}
if (poll.getFinishHeight() <= attachment.getFinishValidationHeight(transaction) + 1) {
throw new NxtException.NotCurrentlyValidException(String.format("Phased transaction finishes at height %d which is not after approval transaction height %d",
poll.getFinishHeight(), attachment.getFinishValidationHeight(transaction) + 1));
}
}
}
@Override
final void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.MessagingPhasingVoteCasting attachment = (Attachment.MessagingPhasingVoteCasting) transaction.getAttachment();
List<byte[]> hashes = attachment.getTransactionFullHashes();
for (byte[] hash : hashes) {
PhasingVote.addVote(transaction, senderAccount, Convert.fullHashToId(hash));
}
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
public static final TransactionType HUB_ANNOUNCEMENT = new Messaging() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_HUB_ANNOUNCEMENT;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.HUB_ANNOUNCEMENT;
}
@Override
public String getName() {
return "HubAnnouncement";
}
@Override
Attachment.MessagingHubAnnouncement parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingHubAnnouncement(buffer, transactionVersion);
}
@Override
Attachment.MessagingHubAnnouncement parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingHubAnnouncement(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.MessagingHubAnnouncement attachment = (Attachment.MessagingHubAnnouncement) transaction.getAttachment();
Hub.addOrUpdateHub(transaction, attachment);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
if (Nxt.getBlockchain().getHeight() < Constants.TRANSPARENT_FORGING_BLOCK_7) {
throw new NxtException.NotYetEnabledException("Hub terminal announcement not yet enabled at height " + Nxt.getBlockchain().getHeight());
}
Attachment.MessagingHubAnnouncement attachment = (Attachment.MessagingHubAnnouncement) transaction.getAttachment();
if (attachment.getMinFeePerByteNQT() < 0 || attachment.getMinFeePerByteNQT() > Constants.MAX_BALANCE_NQT
|| attachment.getUris().length > Constants.MAX_HUB_ANNOUNCEMENT_URIS) {
// cfb: "0" is allowed to show that another way to determine the min fee should be used
throw new NxtException.NotValidException("Invalid hub terminal announcement: " + attachment.getJSONObject());
}
for (String uri : attachment.getUris()) {
if (uri.length() > Constants.MAX_HUB_ANNOUNCEMENT_URI_LENGTH) {
throw new NxtException.NotValidException("Invalid URI length: " + uri.length());
}
//also check URI validity here?
}
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
public static final Messaging ACCOUNT_INFO = new Messaging() {
private final Fee ACCOUNT_INFO_FEE = new Fee.SizeBasedFee(Constants.ONE_NXT, 2 * Constants.ONE_NXT, 32) {
@Override
public int getSize(TransactionImpl transaction, Appendix appendage) {
Attachment.MessagingAccountInfo attachment = (Attachment.MessagingAccountInfo) transaction.getAttachment();
return attachment.getName().length() + attachment.getDescription().length();
}
};
@Override
public byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_ACCOUNT_INFO;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ACCOUNT_INFO;
}
@Override
public String getName() {
return "AccountInfo";
}
@Override
Fee getNextFee(Transaction transaction) {
return ACCOUNT_INFO_FEE;
}
@Override
int getNextFeeHeight() {
return Constants.SHUFFLING_BLOCK;
}
@Override
Attachment.MessagingAccountInfo parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingAccountInfo(buffer, transactionVersion);
}
@Override
Attachment.MessagingAccountInfo parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingAccountInfo(attachmentData);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.MessagingAccountInfo attachment = (Attachment.MessagingAccountInfo)transaction.getAttachment();
if (attachment.getName().length() > Constants.MAX_ACCOUNT_NAME_LENGTH
|| attachment.getDescription().length() > Constants.MAX_ACCOUNT_DESCRIPTION_LENGTH) {
throw new NxtException.NotValidException("Invalid account info issuance: " + attachment.getJSONObject());
}
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.MessagingAccountInfo attachment = (Attachment.MessagingAccountInfo) transaction.getAttachment();
senderAccount.setAccountInfo(attachment.getName(), attachment.getDescription());
}
@Override
boolean isBlockDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
return Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK
&& isDuplicate(Messaging.ACCOUNT_INFO, getName(), duplicates, true);
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
public static final Messaging ACCOUNT_PROPERTY = new Messaging() {
private final Fee ACCOUNT_PROPERTY_FEE = new Fee.SizeBasedFee(Constants.ONE_NXT, Constants.ONE_NXT, 32) {
@Override
public int getSize(TransactionImpl transaction, Appendix appendage) {
Attachment.MessagingAccountProperty attachment = (Attachment.MessagingAccountProperty) transaction.getAttachment();
return attachment.getValue().length();
}
};
@Override
public byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_ACCOUNT_PROPERTY;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ACCOUNT_PROPERTY;
}
@Override
public String getName() {
return "AccountProperty";
}
@Override
Fee getBaselineFee(Transaction transaction) {
return ACCOUNT_PROPERTY_FEE;
}
@Override
Attachment.MessagingAccountProperty parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingAccountProperty(buffer, transactionVersion);
}
@Override
Attachment.MessagingAccountProperty parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingAccountProperty(attachmentData);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) {
throw new NxtException.NotYetEnabledException("Account properties not yet enabled");
}
Attachment.MessagingAccountProperty attachment = (Attachment.MessagingAccountProperty)transaction.getAttachment();
if (attachment.getProperty().length() > Constants.MAX_ACCOUNT_PROPERTY_NAME_LENGTH
|| attachment.getProperty().length() == 0
|| attachment.getValue().length() > Constants.MAX_ACCOUNT_PROPERTY_VALUE_LENGTH) {
throw new NxtException.NotValidException("Invalid account property: " + attachment.getJSONObject());
}
if (transaction.getAmountNQT() != 0) {
throw new NxtException.NotValidException("Account property transaction cannot be used to send NXT");
}
if (transaction.getRecipientId() == Genesis.CREATOR_ID) {
throw new NxtException.NotValidException("Setting Genesis account properties not allowed");
}
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.MessagingAccountProperty attachment = (Attachment.MessagingAccountProperty) transaction.getAttachment();
recipientAccount.setProperty(transaction, senderAccount, attachment.getProperty(), attachment.getValue());
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
public static final Messaging ACCOUNT_PROPERTY_DELETE = new Messaging() {
@Override
public byte getSubtype() {
return TransactionType.SUBTYPE_MESSAGING_ACCOUNT_PROPERTY_DELETE;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ACCOUNT_PROPERTY_DELETE;
}
@Override
public String getName() {
return "AccountPropertyDelete";
}
@Override
Attachment.MessagingAccountPropertyDelete parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.MessagingAccountPropertyDelete(buffer, transactionVersion);
}
@Override
Attachment.MessagingAccountPropertyDelete parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.MessagingAccountPropertyDelete(attachmentData);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) {
throw new NxtException.NotYetEnabledException("Account properties not yet enabled");
}
Attachment.MessagingAccountPropertyDelete attachment = (Attachment.MessagingAccountPropertyDelete)transaction.getAttachment();
Account.AccountProperty accountProperty = Account.getProperty(attachment.getPropertyId());
if (accountProperty == null) {
throw new NxtException.NotCurrentlyValidException("No such property " + Long.toUnsignedString(attachment.getPropertyId()));
}
if (accountProperty.getRecipientId() != transaction.getSenderId() && accountProperty.getSetterId() != transaction.getSenderId()) {
throw new NxtException.NotValidException("Account " + Long.toUnsignedString(transaction.getSenderId())
+ " cannot delete property " + Long.toUnsignedString(attachment.getPropertyId()));
}
if (accountProperty.getRecipientId() != transaction.getRecipientId()) {
throw new NxtException.NotValidException("Account property " + Long.toUnsignedString(attachment.getPropertyId())
+ " does not belong to " + Long.toUnsignedString(transaction.getRecipientId()));
}
if (transaction.getAmountNQT() != 0) {
throw new NxtException.NotValidException("Account property transaction cannot be used to send NXT");
}
if (transaction.getRecipientId() == Genesis.CREATOR_ID) {
throw new NxtException.NotValidException("Deleting Genesis account properties not allowed");
}
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.MessagingAccountPropertyDelete attachment = (Attachment.MessagingAccountPropertyDelete) transaction.getAttachment();
senderAccount.deleteProperty(attachment.getPropertyId());
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
}
public static abstract class ColoredCoins extends TransactionType {
private ColoredCoins() {}
@Override
public final byte getType() {
return TransactionType.TYPE_COLORED_COINS;
}
public static final TransactionType ASSET_ISSUANCE = new ColoredCoins() {
private final Fee ASSET_ISSUANCE_FEE = new Fee.ConstantFee(1000 * Constants.ONE_NXT);
private final Fee SINGLETON_ASSET_FEE = new Fee.SizeBasedFee(Constants.ONE_NXT, Constants.ONE_NXT, 32) {
public int getSize(TransactionImpl transaction, Appendix appendage) {
Attachment.ColoredCoinsAssetIssuance attachment = (Attachment.ColoredCoinsAssetIssuance) transaction.getAttachment();
return attachment.getDescription().length();
}
};
private final Fee ASSET_ISSUANCE_FEE_2 = (transaction, appendage) -> isSingletonIssuance(transaction) ?
SINGLETON_ASSET_FEE.getFee(transaction, appendage) : ASSET_ISSUANCE_FEE.getFee(transaction, appendage);
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_COLORED_COINS_ASSET_ISSUANCE;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ASSET_ISSUANCE;
}
@Override
public String getName() {
return "AssetIssuance";
}
@Override
Fee getBaselineFee(Transaction transaction) {
return ASSET_ISSUANCE_FEE;
}
@Override
Fee getNextFee(Transaction transaction) {
return ASSET_ISSUANCE_FEE_2;
}
@Override
int getNextFeeHeight() {
return Constants.SHUFFLING_BLOCK;
}
@Override
long[] getBackFees(Transaction transaction) {
if (isSingletonIssuance(transaction)) {
return Convert.EMPTY_LONG;
}
long feeNQT = transaction.getFeeNQT();
return new long[] {feeNQT * 3 / 10, feeNQT * 2 / 10, feeNQT / 10};
}
@Override
Attachment.ColoredCoinsAssetIssuance parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsAssetIssuance(buffer, transactionVersion);
}
@Override
Attachment.ColoredCoinsAssetIssuance parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsAssetIssuance(attachmentData);
}
@Override
boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
return true;
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.ColoredCoinsAssetIssuance attachment = (Attachment.ColoredCoinsAssetIssuance) transaction.getAttachment();
long assetId = transaction.getId();
Asset.addAsset(transaction, attachment);
senderAccount.addToAssetAndUnconfirmedAssetBalanceQNT(getLedgerEvent(), assetId, assetId, attachment.getQuantityQNT());
}
@Override
void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.ColoredCoinsAssetIssuance attachment = (Attachment.ColoredCoinsAssetIssuance)transaction.getAttachment();
if (attachment.getName().length() < Constants.MIN_ASSET_NAME_LENGTH
|| attachment.getName().length() > Constants.MAX_ASSET_NAME_LENGTH
|| attachment.getDescription().length() > Constants.MAX_ASSET_DESCRIPTION_LENGTH
|| attachment.getDecimals() < 0 || attachment.getDecimals() > 8
|| attachment.getQuantityQNT() <= 0
|| attachment.getQuantityQNT() > Constants.MAX_ASSET_QUANTITY_QNT
) {
throw new NxtException.NotValidException("Invalid asset issuance: " + attachment.getJSONObject());
}
String normalizedName = attachment.getName().toLowerCase();
for (int i = 0; i < normalizedName.length(); i++) {
if (Constants.ALPHABET.indexOf(normalizedName.charAt(i)) < 0) {
throw new NxtException.NotValidException("Invalid asset name: " + normalizedName);
}
}
}
@Override
boolean isBlockDuplicate(final Transaction transaction, final Map<TransactionType, Map<String, Integer>> duplicates) {
if (Nxt.getBlockchain().getHeight() <= Constants.SHUFFLING_BLOCK) {
return false;
}
if (isSingletonIssuance(transaction)) {
return false;
}
return isDuplicate(ColoredCoins.ASSET_ISSUANCE, getName(), duplicates, true);
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return true;
}
private boolean isSingletonIssuance(Transaction transaction) {
Attachment.ColoredCoinsAssetIssuance attachment = (Attachment.ColoredCoinsAssetIssuance)transaction.getAttachment();
return attachment.getQuantityQNT() == 1 && attachment.getDecimals() == 0
&& attachment.getDescription().length() <= Constants.MAX_SINGLETON_ASSET_DESCRIPTION_LENGTH;
}
};
public static final TransactionType ASSET_TRANSFER = new ColoredCoins() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_COLORED_COINS_ASSET_TRANSFER;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ASSET_TRANSFER;
}
@Override
public String getName() {
return "AssetTransfer";
}
@Override
Attachment.ColoredCoinsAssetTransfer parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsAssetTransfer(buffer, transactionVersion);
}
@Override
Attachment.ColoredCoinsAssetTransfer parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsAssetTransfer(attachmentData);
}
@Override
boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.ColoredCoinsAssetTransfer attachment = (Attachment.ColoredCoinsAssetTransfer) transaction.getAttachment();
long unconfirmedAssetBalance = senderAccount.getUnconfirmedAssetBalanceQNT(attachment.getAssetId());
if (unconfirmedAssetBalance >= 0 && unconfirmedAssetBalance >= attachment.getQuantityQNT()) {
senderAccount.addToUnconfirmedAssetBalanceQNT(getLedgerEvent(), transaction.getId(),
attachment.getAssetId(), -attachment.getQuantityQNT());
return true;
}
return false;
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.ColoredCoinsAssetTransfer attachment = (Attachment.ColoredCoinsAssetTransfer) transaction.getAttachment();
senderAccount.addToAssetBalanceQNT(getLedgerEvent(), transaction.getId(), attachment.getAssetId(),
-attachment.getQuantityQNT());
if (recipientAccount.getId() == Genesis.CREATOR_ID) {
Asset.deleteAsset(transaction, attachment.getAssetId(), attachment.getQuantityQNT());
} else {
recipientAccount.addToAssetAndUnconfirmedAssetBalanceQNT(getLedgerEvent(), transaction.getId(),
attachment.getAssetId(), attachment.getQuantityQNT());
AssetTransfer.addAssetTransfer(transaction, attachment);
}
}
@Override
void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.ColoredCoinsAssetTransfer attachment = (Attachment.ColoredCoinsAssetTransfer) transaction.getAttachment();
senderAccount.addToUnconfirmedAssetBalanceQNT(getLedgerEvent(), transaction.getId(),
attachment.getAssetId(), attachment.getQuantityQNT());
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.ColoredCoinsAssetTransfer attachment = (Attachment.ColoredCoinsAssetTransfer)transaction.getAttachment();
if (transaction.getAmountNQT() != 0
|| attachment.getComment() != null && attachment.getComment().length() > Constants.MAX_ASSET_TRANSFER_COMMENT_LENGTH
|| attachment.getAssetId() == 0) {
throw new NxtException.NotValidException("Invalid asset transfer amount or comment: " + attachment.getJSONObject());
}
if (transaction.getRecipientId() == Genesis.CREATOR_ID && attachment.getFinishValidationHeight(transaction) > Constants.SHUFFLING_BLOCK) {
throw new NxtException.NotCurrentlyValidException("Asset transfer to Genesis no longer allowed, "
+ "use asset delete attachment instead");
}
if (transaction.getVersion() > 0 && attachment.getComment() != null) {
throw new NxtException.NotValidException("Asset transfer comments no longer allowed, use message " +
"or encrypted message appendix instead");
}
Asset asset = Asset.getAsset(attachment.getAssetId());
if (attachment.getQuantityQNT() <= 0 || (asset != null && attachment.getQuantityQNT() > asset.getInitialQuantityQNT())) {
throw new NxtException.NotValidException("Invalid asset transfer asset or quantity: " + attachment.getJSONObject());
}
if (asset == null) {
throw new NxtException.NotCurrentlyValidException("Asset " + Long.toUnsignedString(attachment.getAssetId()) +
" does not exist yet");
}
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
public static final TransactionType ASSET_DELETE = new ColoredCoins() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_COLORED_COINS_ASSET_DELETE;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ASSET_DELETE;
}
@Override
public String getName() {
return "AssetDelete";
}
@Override
Attachment.ColoredCoinsAssetDelete parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsAssetDelete(buffer, transactionVersion);
}
@Override
Attachment.ColoredCoinsAssetDelete parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsAssetDelete(attachmentData);
}
@Override
boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.ColoredCoinsAssetDelete attachment = (Attachment.ColoredCoinsAssetDelete)transaction.getAttachment();
long unconfirmedAssetBalance = senderAccount.getUnconfirmedAssetBalanceQNT(attachment.getAssetId());
if (unconfirmedAssetBalance >= 0 && unconfirmedAssetBalance >= attachment.getQuantityQNT()) {
senderAccount.addToUnconfirmedAssetBalanceQNT(getLedgerEvent(), transaction.getId(),
attachment.getAssetId(), -attachment.getQuantityQNT());
return true;
}
return false;
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.ColoredCoinsAssetDelete attachment = (Attachment.ColoredCoinsAssetDelete)transaction.getAttachment();
senderAccount.addToAssetBalanceQNT(getLedgerEvent(), transaction.getId(), attachment.getAssetId(),
-attachment.getQuantityQNT());
Asset.deleteAsset(transaction, attachment.getAssetId(), attachment.getQuantityQNT());
}
@Override
void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.ColoredCoinsAssetDelete attachment = (Attachment.ColoredCoinsAssetDelete)transaction.getAttachment();
senderAccount.addToUnconfirmedAssetBalanceQNT(getLedgerEvent(), transaction.getId(),
attachment.getAssetId(), attachment.getQuantityQNT());
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) {
throw new NxtException.NotCurrentlyValidException("Asset delete not yet enabled at height " + Nxt.getBlockchain().getHeight());
}
Attachment.ColoredCoinsAssetDelete attachment = (Attachment.ColoredCoinsAssetDelete)transaction.getAttachment();
if (attachment.getAssetId() == 0) {
throw new NxtException.NotValidException("Invalid asset identifier: " + attachment.getJSONObject());
}
Asset asset = Asset.getAsset(attachment.getAssetId());
if (attachment.getQuantityQNT() <= 0 || (asset != null && attachment.getQuantityQNT() > asset.getInitialQuantityQNT())) {
throw new NxtException.NotValidException("Invalid asset delete asset or quantity: " + attachment.getJSONObject());
}
if (asset == null) {
throw new NxtException.NotCurrentlyValidException("Asset " + Long.toUnsignedString(attachment.getAssetId()) +
" does not exist yet");
}
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
abstract static class ColoredCoinsOrderPlacement extends ColoredCoins {
@Override
final void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.ColoredCoinsOrderPlacement attachment = (Attachment.ColoredCoinsOrderPlacement)transaction.getAttachment();
if (attachment.getPriceNQT() <= 0 || attachment.getPriceNQT() > Constants.MAX_BALANCE_NQT
|| attachment.getAssetId() == 0) {
throw new NxtException.NotValidException("Invalid asset order placement: " + attachment.getJSONObject());
}
Asset asset = Asset.getAsset(attachment.getAssetId());
if (attachment.getQuantityQNT() <= 0 || (asset != null && attachment.getQuantityQNT() > asset.getInitialQuantityQNT())) {
throw new NxtException.NotValidException("Invalid asset order placement asset or quantity: " + attachment.getJSONObject());
}
if (asset == null) {
throw new NxtException.NotCurrentlyValidException("Asset " + Long.toUnsignedString(attachment.getAssetId()) +
" does not exist yet");
}
}
@Override
public final boolean canHaveRecipient() {
return false;
}
@Override
public final boolean isPhasingSafe() {
return true;
}
}
public static final TransactionType ASK_ORDER_PLACEMENT = new ColoredCoins.ColoredCoinsOrderPlacement() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_COLORED_COINS_ASK_ORDER_PLACEMENT;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ASSET_ASK_ORDER_PLACEMENT;
}
@Override
public String getName() {
return "AskOrderPlacement";
}
@Override
Attachment.ColoredCoinsAskOrderPlacement parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsAskOrderPlacement(buffer, transactionVersion);
}
@Override
Attachment.ColoredCoinsAskOrderPlacement parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsAskOrderPlacement(attachmentData);
}
@Override
boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.ColoredCoinsAskOrderPlacement attachment = (Attachment.ColoredCoinsAskOrderPlacement) transaction.getAttachment();
long unconfirmedAssetBalance = senderAccount.getUnconfirmedAssetBalanceQNT(attachment.getAssetId());
if (unconfirmedAssetBalance >= 0 && unconfirmedAssetBalance >= attachment.getQuantityQNT()) {
senderAccount.addToUnconfirmedAssetBalanceQNT(getLedgerEvent(), transaction.getId(),
attachment.getAssetId(), -attachment.getQuantityQNT());
return true;
}
return false;
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.ColoredCoinsAskOrderPlacement attachment = (Attachment.ColoredCoinsAskOrderPlacement) transaction.getAttachment();
Order.Ask.addOrder(transaction, attachment);
}
@Override
void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.ColoredCoinsAskOrderPlacement attachment = (Attachment.ColoredCoinsAskOrderPlacement) transaction.getAttachment();
senderAccount.addToUnconfirmedAssetBalanceQNT(getLedgerEvent(), transaction.getId(),
attachment.getAssetId(), attachment.getQuantityQNT());
}
};
public final static TransactionType BID_ORDER_PLACEMENT = new ColoredCoins.ColoredCoinsOrderPlacement() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_COLORED_COINS_BID_ORDER_PLACEMENT;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ASSET_BID_ORDER_PLACEMENT;
}
@Override
public String getName() {
return "BidOrderPlacement";
}
@Override
Attachment.ColoredCoinsBidOrderPlacement parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsBidOrderPlacement(buffer, transactionVersion);
}
@Override
Attachment.ColoredCoinsBidOrderPlacement parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsBidOrderPlacement(attachmentData);
}
@Override
boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.ColoredCoinsBidOrderPlacement attachment = (Attachment.ColoredCoinsBidOrderPlacement) transaction.getAttachment();
if (senderAccount.getUnconfirmedBalanceNQT() >= Math.multiplyExact(attachment.getQuantityQNT(), attachment.getPriceNQT())) {
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(),
-Math.multiplyExact(attachment.getQuantityQNT(), attachment.getPriceNQT()));
return true;
}
return false;
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.ColoredCoinsBidOrderPlacement attachment = (Attachment.ColoredCoinsBidOrderPlacement) transaction.getAttachment();
Order.Bid.addOrder(transaction, attachment);
}
@Override
void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.ColoredCoinsBidOrderPlacement attachment = (Attachment.ColoredCoinsBidOrderPlacement) transaction.getAttachment();
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(),
Math.multiplyExact(attachment.getQuantityQNT(), attachment.getPriceNQT()));
}
};
abstract static class ColoredCoinsOrderCancellation extends ColoredCoins {
@Override
final boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
return true;
}
@Override
final void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
}
@Override
public final boolean canHaveRecipient() {
return false;
}
@Override
public final boolean isPhasingSafe() {
return true;
}
}
public static final TransactionType ASK_ORDER_CANCELLATION = new ColoredCoins.ColoredCoinsOrderCancellation() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_COLORED_COINS_ASK_ORDER_CANCELLATION;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ASSET_ASK_ORDER_CANCELLATION;
}
@Override
public String getName() {
return "AskOrderCancellation";
}
@Override
Attachment.ColoredCoinsAskOrderCancellation parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsAskOrderCancellation(buffer, transactionVersion);
}
@Override
Attachment.ColoredCoinsAskOrderCancellation parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsAskOrderCancellation(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.ColoredCoinsAskOrderCancellation attachment = (Attachment.ColoredCoinsAskOrderCancellation) transaction.getAttachment();
Order order = Order.Ask.getAskOrder(attachment.getOrderId());
Order.Ask.removeOrder(attachment.getOrderId());
if (order != null) {
senderAccount.addToUnconfirmedAssetBalanceQNT(getLedgerEvent(), transaction.getId(),
order.getAssetId(), order.getQuantityQNT());
}
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.ColoredCoinsAskOrderCancellation attachment = (Attachment.ColoredCoinsAskOrderCancellation) transaction.getAttachment();
Order ask = Order.Ask.getAskOrder(attachment.getOrderId());
if (ask == null) {
throw new NxtException.NotCurrentlyValidException("Invalid ask order: " + Long.toUnsignedString(attachment.getOrderId()));
}
if (ask.getAccountId() != transaction.getSenderId()) {
throw new NxtException.NotValidException("Order " + Long.toUnsignedString(attachment.getOrderId()) + " was created by account "
+ Long.toUnsignedString(ask.getAccountId()));
}
}
};
public static final TransactionType BID_ORDER_CANCELLATION = new ColoredCoins.ColoredCoinsOrderCancellation() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_COLORED_COINS_BID_ORDER_CANCELLATION;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ASSET_BID_ORDER_CANCELLATION;
}
@Override
public String getName() {
return "BidOrderCancellation";
}
@Override
Attachment.ColoredCoinsBidOrderCancellation parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsBidOrderCancellation(buffer, transactionVersion);
}
@Override
Attachment.ColoredCoinsBidOrderCancellation parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.ColoredCoinsBidOrderCancellation(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.ColoredCoinsBidOrderCancellation attachment = (Attachment.ColoredCoinsBidOrderCancellation) transaction.getAttachment();
Order order = Order.Bid.getBidOrder(attachment.getOrderId());
Order.Bid.removeOrder(attachment.getOrderId());
if (order != null) {
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(),
Math.multiplyExact(order.getQuantityQNT(), order.getPriceNQT()));
}
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.ColoredCoinsBidOrderCancellation attachment = (Attachment.ColoredCoinsBidOrderCancellation) transaction.getAttachment();
Order bid = Order.Bid.getBidOrder(attachment.getOrderId());
if (bid == null) {
throw new NxtException.NotCurrentlyValidException("Invalid bid order: " + Long.toUnsignedString(attachment.getOrderId()));
}
if (bid.getAccountId() != transaction.getSenderId()) {
throw new NxtException.NotValidException("Order " + Long.toUnsignedString(attachment.getOrderId()) + " was created by account "
+ Long.toUnsignedString(bid.getAccountId()));
}
}
};
public static final TransactionType DIVIDEND_PAYMENT = new ColoredCoins() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_COLORED_COINS_DIVIDEND_PAYMENT;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ASSET_DIVIDEND_PAYMENT;
}
@Override
public String getName() {
return "DividendPayment";
}
@Override
Attachment.ColoredCoinsDividendPayment parseAttachment(ByteBuffer buffer, byte transactionVersion) {
return new Attachment.ColoredCoinsDividendPayment(buffer, transactionVersion);
}
@Override
Attachment.ColoredCoinsDividendPayment parseAttachment(JSONObject attachmentData) {
return new Attachment.ColoredCoinsDividendPayment(attachmentData);
}
@Override
boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.ColoredCoinsDividendPayment attachment = (Attachment.ColoredCoinsDividendPayment)transaction.getAttachment();
long assetId = attachment.getAssetId();
Asset asset = Asset.getAsset(assetId, attachment.getHeight());
//TODO: enable bugfix after hardfork
/*
if (asset == null) {
return true;
}
*/
long quantityQNT = (asset == null ? Asset.getAsset(assetId).getInitialQuantityQNT() : asset.getQuantityQNT())
- senderAccount.getAssetBalanceQNT(assetId, attachment.getHeight());
long totalDividendPayment = Math.multiplyExact(attachment.getAmountNQTPerQNT(), quantityQNT);
if (senderAccount.getUnconfirmedBalanceNQT() >= totalDividendPayment) {
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), -totalDividendPayment);
return true;
}
return false;
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.ColoredCoinsDividendPayment attachment = (Attachment.ColoredCoinsDividendPayment)transaction.getAttachment();
senderAccount.payDividends(transaction.getId(), attachment.getAssetId(), attachment.getHeight(),
attachment.getAmountNQTPerQNT());
}
@Override
void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.ColoredCoinsDividendPayment attachment = (Attachment.ColoredCoinsDividendPayment)transaction.getAttachment();
long assetId = attachment.getAssetId();
Asset asset = Asset.getAsset(assetId, attachment.getHeight());
//TODO: enable bugfix after hardfork
/*
if (asset == null) {
return;
}
*/
long quantityQNT = (asset == null ? Asset.getAsset(assetId).getInitialQuantityQNT() : asset.getQuantityQNT())
- senderAccount.getAssetBalanceQNT(assetId, attachment.getHeight());
long totalDividendPayment = Math.multiplyExact(attachment.getAmountNQTPerQNT(), quantityQNT);
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), totalDividendPayment);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.ColoredCoinsDividendPayment attachment = (Attachment.ColoredCoinsDividendPayment)transaction.getAttachment();
Asset asset;
if (Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK) {
asset = Asset.getAsset(attachment.getAssetId(), attachment.getHeight());
} else {
asset = Asset.getAsset(attachment.getAssetId());
}
if (asset == null) {
throw new NxtException.NotCurrentlyValidException("Asset " + Long.toUnsignedString(attachment.getAssetId())
+ " for dividend payment doesn't exist yet");
}
if (asset.getAccountId() != transaction.getSenderId() || attachment.getAmountNQTPerQNT() <= 0) {
throw new NxtException.NotValidException("Invalid dividend payment sender or amount " + attachment.getJSONObject());
}
if (attachment.getHeight() > Nxt.getBlockchain().getHeight()
|| attachment.getHeight() <= attachment.getFinishValidationHeight(transaction) - Constants.MAX_DIVIDEND_PAYMENT_ROLLBACK) {
throw new NxtException.NotCurrentlyValidException("Invalid dividend payment height: " + attachment.getHeight());
}
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
}
public static abstract class DigitalGoods extends TransactionType {
private DigitalGoods() {
}
@Override
public final byte getType() {
return TransactionType.TYPE_DIGITAL_GOODS;
}
@Override
boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
return true;
}
@Override
void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
}
@Override
final void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
if (transaction.getAmountNQT() != 0) {
throw new NxtException.NotValidException("Invalid digital goods transaction");
}
doValidateAttachment(transaction);
}
abstract void doValidateAttachment(Transaction transaction) throws NxtException.ValidationException;
public static final TransactionType LISTING = new DigitalGoods() {
private final Fee DGS_LISTING_FEE = new Fee.SizeBasedFee(2 * Constants.ONE_NXT, 2 * Constants.ONE_NXT, 32) {
@Override
public int getSize(TransactionImpl transaction, Appendix appendage) {
Attachment.DigitalGoodsListing attachment = (Attachment.DigitalGoodsListing) transaction.getAttachment();
return attachment.getName().length() + attachment.getDescription().length();
}
};
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_DIGITAL_GOODS_LISTING;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.DIGITAL_GOODS_LISTING;
}
@Override
public String getName() {
return "DigitalGoodsListing";
}
@Override
Fee getNextFee(Transaction transaction) {
return DGS_LISTING_FEE;
}
@Override
int getNextFeeHeight() {
return Constants.SHUFFLING_BLOCK;
}
@Override
Attachment.DigitalGoodsListing parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsListing(buffer, transactionVersion);
}
@Override
Attachment.DigitalGoodsListing parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsListing(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.DigitalGoodsListing attachment = (Attachment.DigitalGoodsListing) transaction.getAttachment();
DigitalGoodsStore.listGoods(transaction, attachment);
}
@Override
void doValidateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.DigitalGoodsListing attachment = (Attachment.DigitalGoodsListing) transaction.getAttachment();
if (attachment.getName().length() == 0
|| attachment.getName().length() > Constants.MAX_DGS_LISTING_NAME_LENGTH
|| attachment.getDescription().length() > Constants.MAX_DGS_LISTING_DESCRIPTION_LENGTH
|| attachment.getTags().length() > Constants.MAX_DGS_LISTING_TAGS_LENGTH
|| attachment.getQuantity() < 0 || attachment.getQuantity() > Constants.MAX_DGS_LISTING_QUANTITY
|| attachment.getPriceNQT() <= 0 || attachment.getPriceNQT() > Constants.MAX_BALANCE_NQT) {
throw new NxtException.NotValidException("Invalid digital goods listing: " + attachment.getJSONObject());
}
}
@Override
boolean isBlockDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
return Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK
&& isDuplicate(DigitalGoods.LISTING, getName(), duplicates, true);
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
public static final TransactionType DELISTING = new DigitalGoods() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_DIGITAL_GOODS_DELISTING;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.DIGITAL_GOODS_DELISTING;
}
@Override
public String getName() {
return "DigitalGoodsDelisting";
}
@Override
Attachment.DigitalGoodsDelisting parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsDelisting(buffer, transactionVersion);
}
@Override
Attachment.DigitalGoodsDelisting parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsDelisting(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.DigitalGoodsDelisting attachment = (Attachment.DigitalGoodsDelisting) transaction.getAttachment();
DigitalGoodsStore.delistGoods(attachment.getGoodsId());
}
@Override
void doValidateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.DigitalGoodsDelisting attachment = (Attachment.DigitalGoodsDelisting) transaction.getAttachment();
DigitalGoodsStore.Goods goods = DigitalGoodsStore.Goods.getGoods(attachment.getGoodsId());
if (goods != null && transaction.getSenderId() != goods.getSellerId()) {
throw new NxtException.NotValidException("Invalid digital goods delisting - seller is different: " + attachment.getJSONObject());
}
if (goods == null || goods.isDelisted()) {
throw new NxtException.NotCurrentlyValidException("Goods " + Long.toUnsignedString(attachment.getGoodsId()) +
"not yet listed or already delisted");
}
}
@Override
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
Attachment.DigitalGoodsDelisting attachment = (Attachment.DigitalGoodsDelisting) transaction.getAttachment();
return isDuplicate(DigitalGoods.DELISTING, Long.toUnsignedString(attachment.getGoodsId()), duplicates, true);
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
public static final TransactionType PRICE_CHANGE = new DigitalGoods() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_DIGITAL_GOODS_PRICE_CHANGE;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.DIGITAL_GOODS_PRICE_CHANGE;
}
@Override
public String getName() {
return "DigitalGoodsPriceChange";
}
@Override
Attachment.DigitalGoodsPriceChange parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsPriceChange(buffer, transactionVersion);
}
@Override
Attachment.DigitalGoodsPriceChange parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsPriceChange(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.DigitalGoodsPriceChange attachment = (Attachment.DigitalGoodsPriceChange) transaction.getAttachment();
DigitalGoodsStore.changePrice(attachment.getGoodsId(), attachment.getPriceNQT());
}
@Override
void doValidateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.DigitalGoodsPriceChange attachment = (Attachment.DigitalGoodsPriceChange) transaction.getAttachment();
DigitalGoodsStore.Goods goods = DigitalGoodsStore.Goods.getGoods(attachment.getGoodsId());
if (attachment.getPriceNQT() <= 0 || attachment.getPriceNQT() > Constants.MAX_BALANCE_NQT
|| (goods != null && transaction.getSenderId() != goods.getSellerId())) {
throw new NxtException.NotValidException("Invalid digital goods price change: " + attachment.getJSONObject());
}
if (goods == null || goods.isDelisted()) {
throw new NxtException.NotCurrentlyValidException("Goods " + Long.toUnsignedString(attachment.getGoodsId()) +
"not yet listed or already delisted");
}
}
@Override
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
Attachment.DigitalGoodsPriceChange attachment = (Attachment.DigitalGoodsPriceChange) transaction.getAttachment();
// not a bug, uniqueness is based on DigitalGoods.DELISTING
return isDuplicate(DigitalGoods.DELISTING, Long.toUnsignedString(attachment.getGoodsId()), duplicates, true);
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public static final TransactionType QUANTITY_CHANGE = new DigitalGoods() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_DIGITAL_GOODS_QUANTITY_CHANGE;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.DIGITAL_GOODS_QUANTITY_CHANGE;
}
@Override
public String getName() {
return "DigitalGoodsQuantityChange";
}
@Override
Attachment.DigitalGoodsQuantityChange parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsQuantityChange(buffer, transactionVersion);
}
@Override
Attachment.DigitalGoodsQuantityChange parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsQuantityChange(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.DigitalGoodsQuantityChange attachment = (Attachment.DigitalGoodsQuantityChange) transaction.getAttachment();
DigitalGoodsStore.changeQuantity(attachment.getGoodsId(), attachment.getDeltaQuantity());
}
@Override
void doValidateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.DigitalGoodsQuantityChange attachment = (Attachment.DigitalGoodsQuantityChange) transaction.getAttachment();
DigitalGoodsStore.Goods goods = DigitalGoodsStore.Goods.getGoods(attachment.getGoodsId());
if (attachment.getDeltaQuantity() < -Constants.MAX_DGS_LISTING_QUANTITY
|| attachment.getDeltaQuantity() > Constants.MAX_DGS_LISTING_QUANTITY
|| (goods != null && transaction.getSenderId() != goods.getSellerId())) {
throw new NxtException.NotValidException("Invalid digital goods quantity change: " + attachment.getJSONObject());
}
if (goods == null || goods.isDelisted()) {
throw new NxtException.NotCurrentlyValidException("Goods " + Long.toUnsignedString(attachment.getGoodsId()) +
"not yet listed or already delisted");
}
}
@Override
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
Attachment.DigitalGoodsQuantityChange attachment = (Attachment.DigitalGoodsQuantityChange) transaction.getAttachment();
// not a bug, uniqueness is based on DigitalGoods.DELISTING
return isDuplicate(DigitalGoods.DELISTING, Long.toUnsignedString(attachment.getGoodsId()), duplicates, true);
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public static final TransactionType PURCHASE = new DigitalGoods() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_DIGITAL_GOODS_PURCHASE;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.DIGITAL_GOODS_PURCHASE;
}
@Override
public String getName() {
return "DigitalGoodsPurchase";
}
@Override
Attachment.DigitalGoodsPurchase parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsPurchase(buffer, transactionVersion);
}
@Override
Attachment.DigitalGoodsPurchase parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsPurchase(attachmentData);
}
@Override
boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.DigitalGoodsPurchase attachment = (Attachment.DigitalGoodsPurchase) transaction.getAttachment();
if (senderAccount.getUnconfirmedBalanceNQT() >= Math.multiplyExact((long) attachment.getQuantity(), attachment.getPriceNQT())) {
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(),
-Math.multiplyExact((long) attachment.getQuantity(), attachment.getPriceNQT()));
return true;
}
return false;
}
@Override
void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.DigitalGoodsPurchase attachment = (Attachment.DigitalGoodsPurchase) transaction.getAttachment();
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(),
Math.multiplyExact((long) attachment.getQuantity(), attachment.getPriceNQT()));
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.DigitalGoodsPurchase attachment = (Attachment.DigitalGoodsPurchase) transaction.getAttachment();
DigitalGoodsStore.purchase(transaction, attachment);
}
@Override
void doValidateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.DigitalGoodsPurchase attachment = (Attachment.DigitalGoodsPurchase) transaction.getAttachment();
DigitalGoodsStore.Goods goods = DigitalGoodsStore.Goods.getGoods(attachment.getGoodsId());
if (attachment.getQuantity() <= 0 || attachment.getQuantity() > Constants.MAX_DGS_LISTING_QUANTITY
|| attachment.getPriceNQT() <= 0 || attachment.getPriceNQT() > Constants.MAX_BALANCE_NQT
|| (goods != null && goods.getSellerId() != transaction.getRecipientId())) {
throw new NxtException.NotValidException("Invalid digital goods purchase: " + attachment.getJSONObject());
}
if (transaction.getEncryptedMessage() != null && ! transaction.getEncryptedMessage().isText()) {
throw new NxtException.NotValidException("Only text encrypted messages allowed");
}
if (goods == null || goods.isDelisted()) {
throw new NxtException.NotCurrentlyValidException("Goods " + Long.toUnsignedString(attachment.getGoodsId()) +
"not yet listed or already delisted");
}
if (attachment.getQuantity() > goods.getQuantity() || attachment.getPriceNQT() != goods.getPriceNQT()) {
throw new NxtException.NotCurrentlyValidException("Goods price or quantity changed: " + attachment.getJSONObject());
}
if (attachment.getDeliveryDeadlineTimestamp() <= Nxt.getBlockchain().getLastBlockTimestamp()) {
throw new NxtException.NotCurrentlyValidException("Delivery deadline has already expired: " + attachment.getDeliveryDeadlineTimestamp());
}
}
@Override
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
if (Nxt.getBlockchain().getHeight() < Constants.MONETARY_SYSTEM_BLOCK) {
return false;
}
Attachment.DigitalGoodsPurchase attachment = (Attachment.DigitalGoodsPurchase) transaction.getAttachment();
// not a bug, uniqueness is based on DigitalGoods.DELISTING
return isDuplicate(DigitalGoods.DELISTING, Long.toUnsignedString(attachment.getGoodsId()), duplicates, false);
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public static final TransactionType DELIVERY = new DigitalGoods() {
private final Fee DGS_DELIVERY_FEE = new Fee.SizeBasedFee(Constants.ONE_NXT, 2 * Constants.ONE_NXT, 32) {
@Override
public int getSize(TransactionImpl transaction, Appendix appendage) {
Attachment.DigitalGoodsDelivery attachment = (Attachment.DigitalGoodsDelivery) transaction.getAttachment();
return attachment.getGoodsDataLength() - 16;
}
};
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_DIGITAL_GOODS_DELIVERY;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.DIGITAL_GOODS_DELIVERY;
}
@Override
public String getName() {
return "DigitalGoodsDelivery";
}
@Override
Fee getNextFee(Transaction transaction) {
return DGS_DELIVERY_FEE;
}
@Override
int getNextFeeHeight() {
return Constants.SHUFFLING_BLOCK;
}
@Override
Attachment.DigitalGoodsDelivery parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsDelivery(buffer, transactionVersion);
}
@Override
Attachment.DigitalGoodsDelivery parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
if (attachmentData.get("goodsData") == null) {
return new Attachment.UnencryptedDigitalGoodsDelivery(attachmentData);
}
return new Attachment.DigitalGoodsDelivery(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.DigitalGoodsDelivery attachment = (Attachment.DigitalGoodsDelivery)transaction.getAttachment();
DigitalGoodsStore.deliver(transaction, attachment);
}
@Override
void doValidateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.DigitalGoodsDelivery attachment = (Attachment.DigitalGoodsDelivery) transaction.getAttachment();
DigitalGoodsStore.Purchase purchase = DigitalGoodsStore.Purchase.getPendingPurchase(attachment.getPurchaseId());
if (attachment.getGoodsDataLength() > Constants.MAX_DGS_GOODS_LENGTH) {
throw new NxtException.NotValidException("Invalid digital goods delivery data length: " + attachment.getGoodsDataLength());
}
if (attachment.getGoods() != null) {
if (attachment.getGoods().getData().length == 0 || attachment.getGoods().getNonce().length != 32) {
throw new NxtException.NotValidException("Invalid digital goods delivery: " + attachment.getJSONObject());
}
}
if (attachment.getDiscountNQT() < 0 || attachment.getDiscountNQT() > Constants.MAX_BALANCE_NQT
|| (purchase != null &&
(purchase.getBuyerId() != transaction.getRecipientId()
|| transaction.getSenderId() != purchase.getSellerId()
|| attachment.getDiscountNQT() > Math.multiplyExact(purchase.getPriceNQT(), (long) purchase.getQuantity())))) {
throw new NxtException.NotValidException("Invalid digital goods delivery: " + attachment.getJSONObject());
}
if (purchase == null || purchase.getEncryptedGoods() != null) {
throw new NxtException.NotCurrentlyValidException("Purchase does not exist yet, or already delivered: "
+ attachment.getJSONObject());
}
}
@Override
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
Attachment.DigitalGoodsDelivery attachment = (Attachment.DigitalGoodsDelivery) transaction.getAttachment();
return isDuplicate(DigitalGoods.DELIVERY, Long.toUnsignedString(attachment.getPurchaseId()), duplicates, true);
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public static final TransactionType FEEDBACK = new DigitalGoods() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_DIGITAL_GOODS_FEEDBACK;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.DIGITAL_GOODS_FEEDBACK;
}
@Override
public String getName() {
return "DigitalGoodsFeedback";
}
@Override
Attachment.DigitalGoodsFeedback parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsFeedback(buffer, transactionVersion);
}
@Override
Attachment.DigitalGoodsFeedback parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsFeedback(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.DigitalGoodsFeedback attachment = (Attachment.DigitalGoodsFeedback)transaction.getAttachment();
DigitalGoodsStore.feedback(attachment.getPurchaseId(), transaction.getEncryptedMessage(), transaction.getMessage());
}
@Override
void doValidateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.DigitalGoodsFeedback attachment = (Attachment.DigitalGoodsFeedback) transaction.getAttachment();
DigitalGoodsStore.Purchase purchase = DigitalGoodsStore.Purchase.getPurchase(attachment.getPurchaseId());
if (purchase != null &&
(purchase.getSellerId() != transaction.getRecipientId()
|| transaction.getSenderId() != purchase.getBuyerId())) {
throw new NxtException.NotValidException("Invalid digital goods feedback: " + attachment.getJSONObject());
}
if (transaction.getEncryptedMessage() == null && transaction.getMessage() == null) {
throw new NxtException.NotValidException("Missing feedback message");
}
if (transaction.getEncryptedMessage() != null && ! transaction.getEncryptedMessage().isText()) {
throw new NxtException.NotValidException("Only text encrypted messages allowed");
}
if (transaction.getMessage() != null && ! transaction.getMessage().isText()) {
throw new NxtException.NotValidException("Only text public messages allowed");
}
if (purchase == null || purchase.getEncryptedGoods() == null) {
throw new NxtException.NotCurrentlyValidException("Purchase does not exist yet or not yet delivered");
}
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
public static final TransactionType REFUND = new DigitalGoods() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_DIGITAL_GOODS_REFUND;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.DIGITAL_GOODS_REFUND;
}
@Override
public String getName() {
return "DigitalGoodsRefund";
}
@Override
Attachment.DigitalGoodsRefund parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsRefund(buffer, transactionVersion);
}
@Override
Attachment.DigitalGoodsRefund parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.DigitalGoodsRefund(attachmentData);
}
@Override
boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.DigitalGoodsRefund attachment = (Attachment.DigitalGoodsRefund) transaction.getAttachment();
if (senderAccount.getUnconfirmedBalanceNQT() >= attachment.getRefundNQT()) {
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), -attachment.getRefundNQT());
return true;
}
return false;
}
@Override
void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
Attachment.DigitalGoodsRefund attachment = (Attachment.DigitalGoodsRefund) transaction.getAttachment();
senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), attachment.getRefundNQT());
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.DigitalGoodsRefund attachment = (Attachment.DigitalGoodsRefund) transaction.getAttachment();
DigitalGoodsStore.refund(getLedgerEvent(), transaction.getId(), transaction.getSenderId(),
attachment.getPurchaseId(), attachment.getRefundNQT(), transaction.getEncryptedMessage());
}
@Override
void doValidateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.DigitalGoodsRefund attachment = (Attachment.DigitalGoodsRefund) transaction.getAttachment();
DigitalGoodsStore.Purchase purchase = DigitalGoodsStore.Purchase.getPurchase(attachment.getPurchaseId());
if (attachment.getRefundNQT() < 0 || attachment.getRefundNQT() > Constants.MAX_BALANCE_NQT
|| (purchase != null &&
(purchase.getBuyerId() != transaction.getRecipientId()
|| transaction.getSenderId() != purchase.getSellerId()))) {
throw new NxtException.NotValidException("Invalid digital goods refund: " + attachment.getJSONObject());
}
if (transaction.getEncryptedMessage() != null && ! transaction.getEncryptedMessage().isText()) {
throw new NxtException.NotValidException("Only text encrypted messages allowed");
}
if (purchase == null || purchase.getEncryptedGoods() == null || purchase.getRefundNQT() != 0) {
throw new NxtException.NotCurrentlyValidException("Purchase does not exist or is not delivered or is already refunded");
}
}
@Override
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
Attachment.DigitalGoodsRefund attachment = (Attachment.DigitalGoodsRefund) transaction.getAttachment();
return isDuplicate(DigitalGoods.REFUND, Long.toUnsignedString(attachment.getPurchaseId()), duplicates, true);
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
}
public static abstract class AccountControl extends TransactionType {
private AccountControl() {
}
@Override
public final byte getType() {
return TransactionType.TYPE_ACCOUNT_CONTROL;
}
@Override
final boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
return true;
}
@Override
final void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
}
public static final TransactionType EFFECTIVE_BALANCE_LEASING = new AccountControl() {
@Override
public final byte getSubtype() {
return TransactionType.SUBTYPE_ACCOUNT_CONTROL_EFFECTIVE_BALANCE_LEASING;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ACCOUNT_CONTROL_EFFECTIVE_BALANCE_LEASING;
}
@Override
public String getName() {
return "EffectiveBalanceLeasing";
}
@Override
Attachment.AccountControlEffectiveBalanceLeasing parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.AccountControlEffectiveBalanceLeasing(buffer, transactionVersion);
}
@Override
Attachment.AccountControlEffectiveBalanceLeasing parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.AccountControlEffectiveBalanceLeasing(attachmentData);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.AccountControlEffectiveBalanceLeasing attachment = (Attachment.AccountControlEffectiveBalanceLeasing) transaction.getAttachment();
Account.getAccount(transaction.getSenderId()).leaseEffectiveBalance(transaction.getRecipientId(), attachment.getPeriod());
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.AccountControlEffectiveBalanceLeasing attachment = (Attachment.AccountControlEffectiveBalanceLeasing)transaction.getAttachment();
if (transaction.getSenderId() == transaction.getRecipientId()
|| transaction.getAmountNQT() != 0
|| attachment.getPeriod() < Constants.LEASING_DELAY
|| attachment.getPeriod() > 65535) {
throw new NxtException.NotValidException("Invalid effective balance leasing: "
+ transaction.getJSONObject() + " transaction " + transaction.getStringId());
}
if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK && attachment.getPeriod() > Short.MAX_VALUE) {
throw new NxtException.NotYetEnabledException("Leasing period longer than 32767 not yet enabled");
}
byte[] recipientPublicKey = Account.getPublicKey(transaction.getRecipientId());
if (recipientPublicKey == null && ! transaction.getStringId().equals("5081403377391821646")) {
throw new NxtException.NotCurrentlyValidException("Invalid effective balance leasing: "
+ " recipient account " + transaction.getRecipientId() + " not found or no public key published");
}
if (transaction.getRecipientId() == Genesis.CREATOR_ID) {
throw new NxtException.NotCurrentlyValidException("Leasing to Genesis account not allowed");
}
}
@Override
public boolean canHaveRecipient() {
return true;
}
@Override
public boolean isPhasingSafe() {
return true;
}
};
public static final TransactionType SET_PHASING_ONLY = new AccountControl() {
@Override
public byte getSubtype() {
return SUBTYPE_ACCOUNT_CONTROL_PHASING_ONLY;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.ACCOUNT_CONTROL_PHASING_ONLY;
}
@Override
AbstractAttachment parseAttachment(ByteBuffer buffer, byte transactionVersion) {
return new Attachment.SetPhasingOnly(buffer, transactionVersion);
}
@Override
AbstractAttachment parseAttachment(JSONObject attachmentData) {
return new Attachment.SetPhasingOnly(attachmentData);
}
@Override
void validateAttachment(Transaction transaction) throws ValidationException {
if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) {
throw new NxtException.NotYetEnabledException("Phasing only account control not yet enabled");
}
Attachment.SetPhasingOnly attachment = (Attachment.SetPhasingOnly)transaction.getAttachment();
VotingModel votingModel = attachment.getPhasingParams().getVoteWeighting().getVotingModel();
attachment.getPhasingParams().validate();
if (votingModel == VotingModel.NONE) {
Account senderAccount = Account.getAccount(transaction.getSenderId());
if (senderAccount == null || !senderAccount.getControls().contains(ControlType.PHASING_ONLY)) {
throw new NxtException.NotCurrentlyValidException("Phasing only account control is not currently enabled");
}
} else if (votingModel == VotingModel.TRANSACTION || votingModel == VotingModel.HASH) {
throw new NxtException.NotValidException("Invalid voting model " + votingModel + " for account control");
}
long maxFees = attachment.getMaxFees();
long maxFeesLimit = (attachment.getPhasingParams().getVoteWeighting().isBalanceIndependent() ? 3 : 22) * Constants.ONE_NXT;
if (maxFees < 0 || (maxFees > 0 && maxFees < maxFeesLimit) || maxFees > Constants.MAX_BALANCE_NQT) {
throw new NxtException.NotValidException(String.format("Invalid max fees %f NXT", ((double)maxFees)/Constants.ONE_NXT));
}
short minDuration = attachment.getMinDuration();
if (minDuration < 0 || (minDuration > 0 && minDuration < 3) || minDuration >= Constants.MAX_PHASING_DURATION) {
throw new NxtException.NotValidException("Invalid min duration " + attachment.getMinDuration());
}
short maxDuration = attachment.getMaxDuration();
if (maxDuration < 0 || (maxDuration > 0 && maxDuration < 3) || maxDuration >= Constants.MAX_PHASING_DURATION) {
throw new NxtException.NotValidException("Invalid max duration " + maxDuration);
}
if (minDuration > maxDuration) {
throw new NxtException.NotValidException(String.format("Min duration %d cannot exceed max duration %d ",
minDuration, maxDuration));
}
}
@Override
boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
return TransactionType.isDuplicate(SET_PHASING_ONLY, Long.toUnsignedString(transaction.getSenderId()), duplicates, true);
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.SetPhasingOnly attachment = (Attachment.SetPhasingOnly)transaction.getAttachment();
AccountRestrictions.PhasingOnly.set(senderAccount, attachment);
}
@Override
public boolean canHaveRecipient() {
return false;
}
@Override
public String getName() {
return "SetPhasingOnly";
}
@Override
public boolean isPhasingSafe() {
return false;
}
};
}
public static abstract class Data extends TransactionType {
private static final Fee TAGGED_DATA_FEE = new Fee.SizeBasedFee(Constants.ONE_NXT, Constants.ONE_NXT/10) {
@Override
public int getSize(TransactionImpl transaction, Appendix appendix) {
return appendix.getFullSize();
}
};
private Data() {
}
@Override
public final byte getType() {
return TransactionType.TYPE_DATA;
}
@Override
final Fee getBaselineFee(Transaction transaction) {
return TAGGED_DATA_FEE;
}
@Override
final boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
return true;
}
@Override
final void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
}
@Override
public final boolean canHaveRecipient() {
return false;
}
@Override
public final boolean isPhasingSafe() {
return false;
}
@Override
public final boolean isPhasable() {
return false;
}
public static final TransactionType TAGGED_DATA_UPLOAD = new Data() {
@Override
public byte getSubtype() {
return SUBTYPE_DATA_TAGGED_DATA_UPLOAD;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.TAGGED_DATA_UPLOAD;
}
@Override
Attachment.TaggedDataUpload parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.TaggedDataUpload(buffer, transactionVersion);
}
@Override
Attachment.TaggedDataUpload parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.TaggedDataUpload(attachmentData);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.TaggedDataUpload attachment = (Attachment.TaggedDataUpload) transaction.getAttachment();
if (attachment.getData() == null && Nxt.getEpochTime() - transaction.getTimestamp() < Constants.MIN_PRUNABLE_LIFETIME) {
throw new NxtException.NotCurrentlyValidException("Data has been pruned prematurely");
}
if (attachment.getData() != null) {
if (attachment.getName().length() == 0 || attachment.getName().length() > Constants.MAX_TAGGED_DATA_NAME_LENGTH) {
throw new NxtException.NotValidException("Invalid name length: " + attachment.getName().length());
}
if (attachment.getDescription().length() > Constants.MAX_TAGGED_DATA_DESCRIPTION_LENGTH) {
throw new NxtException.NotValidException("Invalid description length: " + attachment.getDescription().length());
}
if (attachment.getTags().length() > Constants.MAX_TAGGED_DATA_TAGS_LENGTH) {
throw new NxtException.NotValidException("Invalid tags length: " + attachment.getTags().length());
}
if (attachment.getType().length() > Constants.MAX_TAGGED_DATA_TYPE_LENGTH) {
throw new NxtException.NotValidException("Invalid type length: " + attachment.getType().length());
}
if (attachment.getChannel().length() > Constants.MAX_TAGGED_DATA_CHANNEL_LENGTH) {
throw new NxtException.NotValidException("Invalid channel length: " + attachment.getChannel().length());
}
if (attachment.getFilename().length() > Constants.MAX_TAGGED_DATA_FILENAME_LENGTH) {
throw new NxtException.NotValidException("Invalid filename length: " + attachment.getFilename().length());
}
if (attachment.getData().length == 0 || attachment.getData().length > Constants.MAX_TAGGED_DATA_DATA_LENGTH) {
throw new NxtException.NotValidException("Invalid data length: " + attachment.getData().length);
}
}
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.TaggedDataUpload attachment = (Attachment.TaggedDataUpload) transaction.getAttachment();
TaggedData.add(transaction, attachment);
}
@Override
public String getName() {
return "TaggedDataUpload";
}
@Override
boolean isPruned(long transactionId) {
return TaggedData.isPruned(transactionId);
}
};
public static final TransactionType TAGGED_DATA_EXTEND = new Data() {
@Override
public byte getSubtype() {
return SUBTYPE_DATA_TAGGED_DATA_EXTEND;
}
@Override
public LedgerEvent getLedgerEvent() {
return LedgerEvent.TAGGED_DATA_EXTEND;
}
@Override
Attachment.TaggedDataExtend parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException {
return new Attachment.TaggedDataExtend(buffer, transactionVersion);
}
@Override
Attachment.TaggedDataExtend parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException {
return new Attachment.TaggedDataExtend(attachmentData);
}
@Override
void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
Attachment.TaggedDataExtend attachment = (Attachment.TaggedDataExtend) transaction.getAttachment();
if ((attachment.jsonIsPruned() || attachment.getData() == null) && Nxt.getEpochTime() - transaction.getTimestamp() < Constants.MIN_PRUNABLE_LIFETIME) {
throw new NxtException.NotCurrentlyValidException("Data has been pruned prematurely");
}
TransactionImpl uploadTransaction = TransactionDb.findTransaction(attachment.getTaggedDataId(), Nxt.getBlockchain().getHeight());
if (uploadTransaction == null) {
throw new NxtException.NotCurrentlyValidException("No such tagged data upload " + Long.toUnsignedString(attachment.getTaggedDataId()));
}
if (uploadTransaction.getType() != TAGGED_DATA_UPLOAD) {
throw new NxtException.NotValidException("Transaction " + Long.toUnsignedString(attachment.getTaggedDataId())
+ " is not a tagged data upload");
}
if (attachment.getData() != null) {
Attachment.TaggedDataUpload taggedDataUpload = (Attachment.TaggedDataUpload)uploadTransaction.getAttachment();
if (!Arrays.equals(attachment.getHash(), taggedDataUpload.getHash())) {
throw new NxtException.NotValidException("Hashes don't match! Extend hash: " + Convert.toHexString(attachment.getHash())
+ " upload hash: " + Convert.toHexString(taggedDataUpload.getHash()));
}
}
}
@Override
void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) {
Attachment.TaggedDataExtend attachment = (Attachment.TaggedDataExtend) transaction.getAttachment();
TaggedData.extend(transaction, attachment);
}
@Override
public String getName() {
return "TaggedDataExtend";
}
@Override
boolean isPruned(long transactionId) {
return false;
}
};
}
}