Merge branch 'asset-unicode' into launch

This commit is contained in:
catbref 2020-05-19 07:57:06 +01:00
commit ed178e744d
9 changed files with 152 additions and 25 deletions

View File

@ -8,6 +8,7 @@ import org.qortal.data.transaction.UpdateAssetTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Amounts;
import org.qortal.utils.Unicode;
public class Asset {
@ -23,6 +24,7 @@ public class Asset {
// Other useful constants
public static final int MIN_NAME_SIZE = 3;
public static final int MAX_NAME_SIZE = 40;
public static final int MAX_DESCRIPTION_SIZE = 4000;
public static final int MAX_DATA_SIZE = 400000;
@ -49,8 +51,8 @@ public class Asset {
this.assetData = new AssetData(ownerAddress, issueAssetTransactionData.getAssetName(),
issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(),
issueAssetTransactionData.isDivisible(), issueAssetTransactionData.getData(),
issueAssetTransactionData.isUnspendable(),
issueAssetTransactionData.getTxGroupId(), issueAssetTransactionData.getSignature());
issueAssetTransactionData.isUnspendable(), issueAssetTransactionData.getTxGroupId(),
issueAssetTransactionData.getSignature(), issueAssetTransactionData.getReducedAssetName());
}
public Asset(Repository repository, long assetId) throws DataException {
@ -66,6 +68,10 @@ public class Asset {
// Processing
public static String reduceName(String assetName) {
return Unicode.sanitize(assetName);
}
public void issue() throws DataException {
this.repository.getAssetRepository().save(this.assetData);
}

View File

@ -20,11 +20,17 @@ public class AssetData {
private String data;
private boolean isUnspendable;
private int creationGroupId;
// No need to expose this via API
@XmlTransient
@Schema(hidden = true)
private byte[] reference;
// For internal use only
@XmlTransient
@Schema(hidden = true)
private String reducedAssetName;
// Constructors
// necessary for JAXB serialization
@ -32,7 +38,8 @@ public class AssetData {
}
// NOTE: key is Long, not long, because it can be null if asset ID/key not yet assigned.
public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible, String data, boolean isUnspendable, int creationGroupId, byte[] reference) {
public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible,
String data, boolean isUnspendable, int creationGroupId, byte[] reference, String reducedAssetName) {
this.assetId = assetId;
this.owner = owner;
this.name = name;
@ -43,11 +50,13 @@ public class AssetData {
this.isUnspendable = isUnspendable;
this.creationGroupId = creationGroupId;
this.reference = reference;
this.reducedAssetName = reducedAssetName;
}
// New asset with unassigned assetId
public AssetData(String owner, String name, String description, long quantity, boolean isDivisible, String data, boolean isUnspendable, int creationGroupId, byte[] reference) {
this(null, owner, name, description, quantity, isDivisible, data, isUnspendable, creationGroupId, reference);
public AssetData(String owner, String name, String description, long quantity, boolean isDivisible, String data,
boolean isUnspendable, int creationGroupId, byte[] reference, String reducedAssetName) {
this(null, owner, name, description, quantity, isDivisible, data, isUnspendable, creationGroupId, reference, reducedAssetName);
}
// Getters/Setters
@ -112,4 +121,12 @@ public class AssetData {
this.reference = reference;
}
public String getReducedAssetName() {
return this.reducedAssetName;
}
public void setReducedAssetName(String reducedAssetName) {
this.reducedAssetName = reducedAssetName;
}
}

View File

@ -3,10 +3,12 @@ package org.qortal.data.transaction;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
import org.qortal.account.NullAccount;
import org.qortal.asset.Asset;
import org.qortal.block.GenesisBlock;
import org.qortal.transaction.Transaction.TransactionType;
@ -48,6 +50,11 @@ public class IssueAssetTransactionData extends TransactionData {
@Schema(description = "whether non-owner holders of asset are barred from using asset", example = "false")
private boolean isUnspendable;
// For internal use
@Schema(hidden = true)
@XmlTransient
private String reducedAssetName;
// Constructors
// For JAXB
@ -64,12 +71,20 @@ public class IssueAssetTransactionData extends TransactionData {
if (parent instanceof GenesisBlock.GenesisInfo && this.issuerPublicKey == null)
this.issuerPublicKey = NullAccount.PUBLIC_KEY;
/*
* If we're being constructed as part of the genesis block info inside blockchain config
* then we need to construct 'reduced' form of asset name.
*/
if (parent instanceof GenesisBlock.GenesisInfo && this.reducedAssetName == null)
this.reducedAssetName = Asset.reduceName(this.assetName);
this.creatorPublicKey = this.issuerPublicKey;
}
/** From repository */
public IssueAssetTransactionData(BaseTransactionData baseTransactionData,
Long assetId, String assetName, String description, long quantity, boolean isDivisible, String data, boolean isUnspendable) {
public IssueAssetTransactionData(BaseTransactionData baseTransactionData, Long assetId, String assetName,
String description, long quantity, boolean isDivisible, String data, boolean isUnspendable,
String reducedAssetName) {
super(TransactionType.ISSUE_ASSET, baseTransactionData);
this.assetId = assetId;
@ -80,12 +95,13 @@ public class IssueAssetTransactionData extends TransactionData {
this.isDivisible = isDivisible;
this.data = data;
this.isUnspendable = isUnspendable;
this.reducedAssetName = reducedAssetName;
}
/** From network/API */
public IssueAssetTransactionData(BaseTransactionData baseTransactionData, String assetName, String description,
long quantity, boolean isDivisible, String data, boolean isUnspendable) {
this(baseTransactionData, null, assetName, description, quantity, isDivisible, data, isUnspendable);
this(baseTransactionData, null, assetName, description, quantity, isDivisible, data, isUnspendable, null);
}
// Getters/Setters
@ -126,4 +142,12 @@ public class IssueAssetTransactionData extends TransactionData {
return this.isUnspendable;
}
public String getReducedAssetName() {
return this.reducedAssetName;
}
public void setReducedAssetName(String reducedAssetName) {
this.reducedAssetName = reducedAssetName;
}
}

View File

@ -19,6 +19,8 @@ public interface AssetRepository {
public boolean assetExists(String assetName) throws DataException;
public boolean reducedAssetNameExists(String reducedAssetName) throws DataException;
public List<AssetData> getAllAssets(Integer limit, Integer offset, Boolean reverse) throws DataException;
public default List<AssetData> getAllAssets() throws DataException {

View File

@ -25,7 +25,9 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public AssetData fromAssetId(long assetId) throws DataException {
String sql = "SELECT owner, asset_name, description, quantity, is_divisible, data, is_unspendable, creation_group_id, reference FROM Assets WHERE asset_id = ?";
String sql = "SELECT owner, asset_name, description, quantity, is_divisible, data, "
+ "is_unspendable, creation_group_id, reference, reduced_asset_name "
+ "FROM Assets WHERE asset_id = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, assetId)) {
if (resultSet == null)
@ -40,9 +42,10 @@ public class HSQLDBAssetRepository implements AssetRepository {
boolean isUnspendable = resultSet.getBoolean(7);
int creationGroupId = resultSet.getInt(8);
byte[] reference = resultSet.getBytes(9);
String reducedAssetName = resultSet.getString(10);
return new AssetData(assetId, owner, assetName, description, quantity, isDivisible, data, isUnspendable,
creationGroupId, reference);
creationGroupId, reference, reducedAssetName);
} catch (SQLException e) {
throw new DataException("Unable to fetch asset from repository", e);
}
@ -50,7 +53,9 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public AssetData fromAssetName(String assetName) throws DataException {
String sql = "SELECT owner, asset_id, description, quantity, is_divisible, data, is_unspendable, creation_group_id, reference FROM Assets WHERE asset_name = ?";
String sql = "SELECT owner, asset_id, description, quantity, is_divisible, data, "
+ "is_unspendable, creation_group_id, reference, reduced_asset_name "
+ "FROM Assets WHERE asset_name = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, assetName)) {
if (resultSet == null)
@ -65,9 +70,10 @@ public class HSQLDBAssetRepository implements AssetRepository {
boolean isUnspendable = resultSet.getBoolean(7);
int creationGroupId = resultSet.getInt(8);
byte[] reference = resultSet.getBytes(9);
String reducedAssetName = resultSet.getString(10);
return new AssetData(assetId, owner, assetName, description, quantity, isDivisible, data, isUnspendable,
creationGroupId, reference);
creationGroupId, reference, reducedAssetName);
} catch (SQLException e) {
throw new DataException("Unable to fetch asset from repository", e);
}
@ -91,10 +97,20 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public boolean reducedAssetNameExists(String reducedAssetName) throws DataException {
try {
return this.repository.exists("Assets", "reduced_asset_name = ?", reducedAssetName);
} catch (SQLException e) {
throw new DataException("Unable to check for asset in repository", e);
}
}
@Override
public List<AssetData> getAllAssets(Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(256);
sql.append("SELECT asset_id, owner, asset_name, description, quantity, is_divisible, data, is_unspendable, creation_group_id, reference "
sql.append("SELECT asset_id, owner, asset_name, description, quantity, is_divisible, data, "
+ "is_unspendable, creation_group_id, reference, reduced_asset_name "
+ "FROM Assets ORDER BY asset_id");
if (reverse != null && reverse)
sql.append(" DESC");
@ -118,9 +134,10 @@ public class HSQLDBAssetRepository implements AssetRepository {
boolean isUnspendable = resultSet.getBoolean(8);
int creationGroupId = resultSet.getInt(9);
byte[] reference = resultSet.getBytes(10);
String reducedAssetName = resultSet.getString(11);
assets.add(new AssetData(assetId, owner, assetName, description, quantity, isDivisible, data,
isUnspendable,creationGroupId, reference));
isUnspendable,creationGroupId, reference, reducedAssetName));
} while (resultSet.next());
return assets;
@ -161,7 +178,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
.bind("asset_name", assetData.getName()).bind("description", assetData.getDescription())
.bind("quantity", assetData.getQuantity()).bind("is_divisible", assetData.isDivisible())
.bind("data", assetData.getData()).bind("is_unspendable", assetData.isUnspendable())
.bind("creation_group_id", assetData.getCreationGroupId()).bind("reference", assetData.getReference());
.bind("creation_group_id", assetData.getCreationGroupId()).bind("reference", assetData.getReference())
.bind("reduced_asset_name", assetData.getReducedAssetName());
try {
saveHelper.execute(this.repository);

View File

@ -355,9 +355,11 @@ public class HSQLDBDatabaseUpdates {
+ "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, "
+ "is_unspendable BOOLEAN NOT NULL DEFAULT FALSE, creation_group_id GroupID NOT NULL DEFAULT 0, "
+ "reference Signature NOT NULL, data AssetData NOT NULL DEFAULT '', "
+ "PRIMARY KEY (asset_id))");
+ "reduced_asset_name AssetName NOT NULL, PRIMARY KEY (asset_id))");
// For when a user wants to lookup an asset by name
stmt.execute("CREATE INDEX AssetNameIndex on Assets (asset_name)");
// For looking up assets by 'reduced' name
stmt.execute("CREATE INDEX AssetReducedNameIndex on Assets (reduced_asset_name)");
// We need a corresponding trigger to make sure new asset_id values are assigned sequentially start from 0
stmt.execute("CREATE TRIGGER Asset_ID_Trigger BEFORE INSERT ON Assets "
@ -386,7 +388,8 @@ public class HSQLDBDatabaseUpdates {
// Issue Asset Transactions
stmt.execute("CREATE TABLE IssueAssetTransactions (signature Signature, issuer QortalPublicKey NOT NULL, asset_name AssetName NOT NULL, "
+ "description GenericDescription NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, asset_id AssetID, "
+ "is_unspendable BOOLEAN NOT NULL, data AssetData NOT NULL DEFAULT '', " + TRANSACTION_KEYS + ")");
+ "is_unspendable BOOLEAN NOT NULL, data AssetData NOT NULL DEFAULT '', reduced_asset_name AssetName NOT NULL, "
+ TRANSACTION_KEYS + ")");
// Transfer Asset Transactions
stmt.execute("CREATE TABLE TransferAssetTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, "

View File

@ -17,7 +17,8 @@ public class HSQLDBIssueAssetTransactionRepository extends HSQLDBTransactionRepo
}
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT asset_name, description, quantity, is_divisible, data, is_unspendable, asset_id FROM IssueAssetTransactions WHERE signature = ?";
String sql = "SELECT asset_name, description, quantity, is_divisible, data, is_unspendable, asset_id, reduced_asset_name "
+ "FROM IssueAssetTransactions WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
if (resultSet == null)
@ -35,8 +36,10 @@ public class HSQLDBIssueAssetTransactionRepository extends HSQLDBTransactionRepo
if (assetId == 0 && resultSet.wasNull())
assetId = null;
String reducedAssetName = resultSet.getString(8);
return new IssueAssetTransactionData(baseTransactionData, assetId, assetName, description, quantity, isDivisible,
data, isUnspendable);
data, isUnspendable, reducedAssetName);
} catch (SQLException e) {
throw new DataException("Unable to fetch issue asset transaction from repository", e);
}
@ -49,7 +52,7 @@ public class HSQLDBIssueAssetTransactionRepository extends HSQLDBTransactionRepo
HSQLDBSaver saveHelper = new HSQLDBSaver("IssueAssetTransactions");
saveHelper.bind("signature", issueAssetTransactionData.getSignature()).bind("issuer", issueAssetTransactionData.getIssuerPublicKey())
.bind("asset_name", issueAssetTransactionData.getAssetName())
.bind("asset_name", issueAssetTransactionData.getAssetName()).bind("reduced_asset_name", issueAssetTransactionData.getReducedAssetName())
.bind("description", issueAssetTransactionData.getDescription()).bind("quantity", issueAssetTransactionData.getQuantity())
.bind("is_divisible", issueAssetTransactionData.isDivisible()).bind("data", issueAssetTransactionData.getData())
.bind("is_unspendable", issueAssetTransactionData.isUnspendable()).bind("asset_id", issueAssetTransactionData.getAssetId());

View File

@ -7,9 +7,11 @@ import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.data.transaction.IssueAssetTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.naming.Name;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Amounts;
import org.qortal.utils.Unicode;
import com.google.common.base.Utf8;
@ -40,15 +42,29 @@ public class IssueAssetTransaction extends Transaction {
return this.getCreator();
}
private String getReducedAssetName() {
if (this.issueAssetTransactionData.getReducedAssetName() == null) {
String reducedAssetName = Name.reduceName(this.issueAssetTransactionData.getAssetName());
this.issueAssetTransactionData.setReducedAssetName(reducedAssetName);
}
return this.issueAssetTransactionData.getReducedAssetName();
}
// Processing
@Override
public ValidationResult isValid() throws DataException {
// Check name size bounds
int assetNameLength = Utf8.encodedLength(this.issueAssetTransactionData.getAssetName());
if (assetNameLength < 1 || assetNameLength > Asset.MAX_NAME_SIZE)
String assetName = this.issueAssetTransactionData.getAssetName();
int assetNameLength = Utf8.encodedLength(assetName);
if (assetNameLength < Asset.MIN_NAME_SIZE || assetNameLength > Asset.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
// Check name is in normalized form (no leading/trailing whitespace, etc.)
if (!assetName.equals(Unicode.normalize(assetName)))
return ValidationResult.NAME_NOT_LOWER_CASE;
// Check description size bounds
int assetDescriptionlength = Utf8.encodedLength(this.issueAssetTransactionData.getDescription());
if (assetDescriptionlength < 1 || assetDescriptionlength > Asset.MAX_DESCRIPTION_SIZE)
@ -74,13 +90,16 @@ public class IssueAssetTransaction extends Transaction {
if (issuer.getConfirmedBalance(Asset.QORT) < this.issueAssetTransactionData.getFee())
return ValidationResult.NO_BALANCE;
// Fill in missing reduced name. Caller is likely to save this as next step.
getReducedAssetName();
return ValidationResult.OK;
}
@Override
public ValidationResult isProcessable() throws DataException {
// Check the asset name isn't already taken.
if (this.repository.getAssetRepository().assetExists(this.issueAssetTransactionData.getAssetName()))
// Check the name isn't already taken
if (this.repository.getAssetRepository().reducedAssetNameExists(getReducedAssetName()))
return ValidationResult.ASSET_ALREADY_EXISTS;
return ValidationResult.OK;

View File

@ -1,12 +1,21 @@
package org.qortal.test.assets;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.data.transaction.IssueAssetTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.Common;
import org.qortal.test.common.TestAccount;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.utils.Amounts;
public class MiscTests extends Common {
@ -21,6 +30,32 @@ public class MiscTests extends Common {
Common.orphanCheck();
}
@Test
public void testCreateAssetWithExistingName() throws DataException {
try (Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
String assetName = "test-asset";
String description = "description";
long quantity = 12345678L;
boolean isDivisible = true;
String data = "{}";
boolean isUnspendable = false;
TransactionData transactionData = new IssueAssetTransactionData(TestTransaction.generateBase(alice), assetName, description, quantity, isDivisible, data, isUnspendable);
TransactionUtils.signAndMint(repository, transactionData, alice);
String duplicateAssetName = "TEST-Ásset";
transactionData = new IssueAssetTransactionData(TestTransaction.generateBase(alice), duplicateAssetName, description, quantity, isDivisible, data, isUnspendable);
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(alice);
ValidationResult result = transaction.importAsUnconfirmed();
assertTrue("Transaction should be invalid", ValidationResult.OK != result);
}
}
@Test
public void testCalcCommitmentWithRoundUp() throws DataException {
long amount = 1234_87654321L;