/******************************************************************************
* 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.crypto.HashFunction;
import java.util.EnumSet;
import java.util.Set;
/**
* Define and validate currency capabilities
*/
public enum CurrencyType {
/**
* Can be exchanged from/to NXT<br>
*/
EXCHANGEABLE(0x01) {
@Override
void validate(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.NotValidException {
}
@Override
void validateMissing(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.NotValidException {
if (transaction.getType() == MonetarySystem.CURRENCY_ISSUANCE) {
if (!validators.contains(CLAIMABLE)) {
throw new NxtException.NotValidException("Currency is not exchangeable and not claimable");
}
}
if (transaction.getType() instanceof MonetarySystem.MonetarySystemExchange || transaction.getType() == MonetarySystem.PUBLISH_EXCHANGE_OFFER) {
throw new NxtException.NotValidException("Currency is not exchangeable");
}
}
},
/**
* Transfers are only allowed from/to issuer account<br>
* Only issuer account can publish exchange offer<br>
*/
CONTROLLABLE(0x02) {
@Override
void validate(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.NotValidException {
if (transaction.getType() == MonetarySystem.CURRENCY_TRANSFER) {
if (currency == null || (currency.getAccountId() != transaction.getSenderId() && currency.getAccountId() != transaction.getRecipientId())) {
throw new NxtException.NotValidException("Controllable currency can only be transferred to/from issuer account");
}
}
if (transaction.getType() == MonetarySystem.PUBLISH_EXCHANGE_OFFER) {
if (currency == null || currency.getAccountId() != transaction.getSenderId()) {
throw new NxtException.NotValidException("Only currency issuer can publish an exchange offer for controllable currency");
}
}
}
@Override
void validateMissing(Currency currency, Transaction transaction, Set<CurrencyType> validators) {}
},
/**
* Can be reserved before the currency is active, reserve is distributed to founders once the currency becomes active<br>
*/
RESERVABLE(0x04) {
@Override
void validate(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.ValidationException {
if (transaction.getType() == MonetarySystem.CURRENCY_ISSUANCE) {
Attachment.MonetarySystemCurrencyIssuance attachment = (Attachment.MonetarySystemCurrencyIssuance) transaction.getAttachment();
int issuanceHeight = attachment.getIssuanceHeight();
int finishHeight = attachment.getFinishValidationHeight(transaction);
if (issuanceHeight <= finishHeight) {
throw new NxtException.NotCurrentlyValidException(
String.format("Reservable currency activation height %d not higher than transaction apply height %d",
issuanceHeight, finishHeight));
}
if (attachment.getMinReservePerUnitNQT() <= 0) {
throw new NxtException.NotValidException("Minimum reserve per unit must be > 0");
}
if (Math.multiplyExact(attachment.getMinReservePerUnitNQT(), attachment.getReserveSupply()) > Constants.MAX_BALANCE_NQT) {
throw new NxtException.NotValidException("Minimum reserve per unit is too large");
}
if (attachment.getReserveSupply() <= attachment.getInitialSupply()) {
throw new NxtException.NotValidException("Reserve supply must exceed initial supply");
}
if (!validators.contains(MINTABLE) && attachment.getReserveSupply() < attachment.getMaxSupply()) {
throw new NxtException.NotValidException("Max supply must not exceed reserve supply for reservable and non-mintable currency");
}
}
if (transaction.getType() == MonetarySystem.RESERVE_INCREASE) {
Attachment.MonetarySystemReserveIncrease attachment = (Attachment.MonetarySystemReserveIncrease) transaction.getAttachment();
if (currency != null && currency.getIssuanceHeight() <= attachment.getFinishValidationHeight(transaction)) {
throw new NxtException.NotCurrentlyValidException("Cannot increase reserve for active currency");
}
}
}
@Override
void validateMissing(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.NotValidException {
if (transaction.getType() == MonetarySystem.RESERVE_INCREASE) {
throw new NxtException.NotValidException("Cannot increase reserve since currency is not reservable");
}
if (transaction.getType() == MonetarySystem.CURRENCY_ISSUANCE) {
Attachment.MonetarySystemCurrencyIssuance attachment = (Attachment.MonetarySystemCurrencyIssuance) transaction.getAttachment();
if (attachment.getIssuanceHeight() != 0) {
throw new NxtException.NotValidException("Issuance height for non-reservable currency must be 0");
}
if (attachment.getMinReservePerUnitNQT() > 0) {
throw new NxtException.NotValidException("Minimum reserve per unit for non-reservable currency must be 0 ");
}
if (attachment.getReserveSupply() > 0) {
throw new NxtException.NotValidException("Reserve supply for non-reservable currency must be 0");
}
if (!validators.contains(MINTABLE) && attachment.getInitialSupply() < attachment.getMaxSupply()) {
throw new NxtException.NotValidException("Initial supply for non-reservable and non-mintable currency must be equal to max supply");
}
}
}
},
/**
* Is {@link #RESERVABLE} and can be claimed after currency is active<br>
* Cannot be {@link #EXCHANGEABLE}
*/
CLAIMABLE(0x08) {
@Override
void validate(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.ValidationException {
if (transaction.getType() == MonetarySystem.CURRENCY_ISSUANCE) {
Attachment.MonetarySystemCurrencyIssuance attachment = (Attachment.MonetarySystemCurrencyIssuance) transaction.getAttachment();
if (!validators.contains(RESERVABLE)) {
throw new NxtException.NotValidException("Claimable currency must be reservable");
}
if (validators.contains(MINTABLE)) {
throw new NxtException.NotValidException("Claimable currency cannot be mintable");
}
if (attachment.getInitialSupply() > 0) {
throw new NxtException.NotValidException("Claimable currency must have initial supply 0");
}
}
if (transaction.getType() == MonetarySystem.RESERVE_CLAIM) {
if (currency == null || !currency.isActive()) {
throw new NxtException.NotCurrentlyValidException("Cannot claim reserve since currency is not yet active");
}
}
}
@Override
void validateMissing(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.NotValidException {
if (transaction.getType() == MonetarySystem.RESERVE_CLAIM) {
throw new NxtException.NotValidException("Cannot claim reserve since currency is not claimable");
}
}
},
/**
* Can be minted using proof of work algorithm<br>
*/
MINTABLE(0x10) {
@Override
void validate(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.NotValidException {
if (transaction.getType() == MonetarySystem.CURRENCY_ISSUANCE) {
Attachment.MonetarySystemCurrencyIssuance issuanceAttachment = (Attachment.MonetarySystemCurrencyIssuance) transaction.getAttachment();
try {
HashFunction hashFunction = HashFunction.getHashFunction(issuanceAttachment.getAlgorithm());
if (!CurrencyMinting.acceptedHashFunctions.contains(hashFunction)) {
throw new NxtException.NotValidException("Invalid minting algorithm " + hashFunction);
}
} catch (IllegalArgumentException e) {
throw new NxtException.NotValidException("Illegal algorithm code specified" , e);
}
if (issuanceAttachment.getMinDifficulty() < 1 || issuanceAttachment.getMaxDifficulty() > 255 ||
issuanceAttachment.getMaxDifficulty() < issuanceAttachment.getMinDifficulty()) {
throw new NxtException.NotValidException(
String.format("Invalid minting difficulties min %d max %d, difficulty must be between 1 and 255, max larger than min",
issuanceAttachment.getMinDifficulty(), issuanceAttachment.getMaxDifficulty()));
}
if (issuanceAttachment.getMaxSupply() <= issuanceAttachment.getReserveSupply()) {
throw new NxtException.NotValidException("Max supply for mintable currency must exceed reserve supply");
}
}
}
@Override
void validateMissing(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.NotValidException {
if (transaction.getType() == MonetarySystem.CURRENCY_ISSUANCE) {
Attachment.MonetarySystemCurrencyIssuance issuanceAttachment = (Attachment.MonetarySystemCurrencyIssuance) transaction.getAttachment();
if (issuanceAttachment.getMinDifficulty() != 0 ||
issuanceAttachment.getMaxDifficulty() != 0 ||
issuanceAttachment.getAlgorithm() != 0) {
throw new NxtException.NotValidException("Non mintable currency should not specify algorithm or difficulty");
}
}
if (transaction.getType() == MonetarySystem.CURRENCY_MINTING) {
throw new NxtException.NotValidException("Currency is not mintable");
}
}
},
/**
* Several accounts can shuffle their currency units and then distributed to recipients<br>
*/
NON_SHUFFLEABLE(0x20) {
@Override
void validate(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.ValidationException {
if (transaction.getType() == ShufflingTransaction.SHUFFLING_CREATION) {
throw new NxtException.NotValidException("Shuffling is not allowed for this currency");
}
}
@Override
void validateMissing(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.ValidationException {
}
};
private final int code;
CurrencyType(int code) {
this.code = code;
}
public int getCode() {
return code;
}
abstract void validate(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.ValidationException;
abstract void validateMissing(Currency currency, Transaction transaction, Set<CurrencyType> validators) throws NxtException.ValidationException;
public static CurrencyType get(int code) {
for (CurrencyType currencyType : values()) {
if (currencyType.getCode() == code) {
return currencyType;
}
}
return null;
}
static void validate(Currency currency, Transaction transaction) throws NxtException.ValidationException {
if (currency == null) {
throw new NxtException.NotCurrentlyValidException("Unknown currency: " + transaction.getAttachment().getJSONObject());
}
validate(currency, currency.getType(), transaction);
}
static void validate(int type, Transaction transaction) throws NxtException.ValidationException {
validate(null, type, transaction);
}
private static void validate(Currency currency, int type, Transaction transaction) throws NxtException.ValidationException {
if (transaction.getAmountNQT() != 0) {
throw new NxtException.NotValidException("Currency transaction NXT amount must be 0");
}
final EnumSet<CurrencyType> validators = EnumSet.noneOf(CurrencyType.class);
for (CurrencyType validator : CurrencyType.values()) {
if ((validator.getCode() & type) != 0) {
validators.add(validator);
}
}
if (validators.isEmpty()) {
throw new NxtException.NotValidException("Currency type not specified");
}
for (CurrencyType validator : CurrencyType.values()) {
if ((validator.getCode() & type) != 0) {
validator.validate(currency, transaction, validators);
} else {
validator.validateMissing(currency, transaction, validators);
}
}
}
static void validateCurrencyNaming(long issuerAccountId, Attachment.MonetarySystemCurrencyIssuance attachment) throws NxtException.ValidationException {
String name = attachment.getName();
String code = attachment.getCode();
String description = attachment.getDescription();
if (name.length() < Constants.MIN_CURRENCY_NAME_LENGTH || name.length() > Constants.MAX_CURRENCY_NAME_LENGTH
|| name.length() < code.length()
|| code.length() < Constants.MIN_CURRENCY_CODE_LENGTH || code.length() > Constants.MAX_CURRENCY_CODE_LENGTH
|| description.length() > Constants.MAX_CURRENCY_DESCRIPTION_LENGTH) {
throw new NxtException.NotValidException(String.format("Invalid currency name %s code %s or description %s", name, code, description));
}
String normalizedName = name.toLowerCase();
for (int i = 0; i < normalizedName.length(); i++) {
if (Constants.ALPHABET.indexOf(normalizedName.charAt(i)) < 0) {
throw new NxtException.NotValidException("Invalid currency name: " + normalizedName);
}
}
for (int i = 0; i < code.length(); i++) {
if (Constants.ALLOWED_CURRENCY_CODE_LETTERS.indexOf(code.charAt(i)) < 0) {
throw new NxtException.NotValidException("Invalid currency code: " + code + " code must be all upper case");
}
}
if (code.contains("NXT") || code.contains("NEXT") || "nxt".equals(normalizedName) || "next".equals(normalizedName)) {
throw new NxtException.NotValidException("Currency name already used");
}
Currency currency;
if ((currency = Currency.getCurrencyByName(normalizedName)) != null && ! currency.canBeDeletedBy(issuerAccountId)) {
throw new NxtException.NotCurrentlyValidException("Currency name already used: " + normalizedName);
}
if ((currency = Currency.getCurrencyByCode(name)) != null && ! currency.canBeDeletedBy(issuerAccountId)) {
throw new NxtException.NotCurrentlyValidException("Currency name already used as code: " + normalizedName);
}
if ((currency = Currency.getCurrencyByCode(code)) != null && ! currency.canBeDeletedBy(issuerAccountId)) {
throw new NxtException.NotCurrentlyValidException("Currency code already used: " + code);
}
if ((currency = Currency.getCurrencyByName(code)) != null && ! currency.canBeDeletedBy(issuerAccountId)) {
throw new NxtException.NotCurrentlyValidException("Currency code already used as name: " + code);
}
}
}