Split AT state storage into two HSQLDB table for better management

This involves a database reshape, but before this happens the node-local
data is exported to local files, giving the user the option to use a
bootstrap file instead of waiting.
This commit is contained in:
catbref 2020-11-04 20:07:30 +00:00
parent b3f859f290
commit 20777363cf
4 changed files with 537 additions and 23 deletions

View File

@ -250,7 +250,8 @@ public class HSQLDBATRepository implements ATRepository {
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
String sql = "SELECT state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE AT_address = ? AND height = ? "
+ "LEFT OUTER JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ? AND ATStates.height = ? "
+ "LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress, height)) {
@ -272,10 +273,11 @@ public class HSQLDBATRepository implements ATRepository {
public ATStateData getLatestATState(String atAddress) throws DataException {
String sql = "SELECT height, state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE AT_address = ? "
// AT_address then height so the compound primary key is used as an index
// Both must be the same direction also
+ "ORDER BY AT_address DESC, height DESC "
+ "JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ? "
// Order by AT_address and height to use compound primary key as index
// Both must be the same direction (DESC) also
+ "ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
+ "LIMIT 1 ";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
@ -306,16 +308,17 @@ public class HSQLDBATRepository implements ATRepository {
+ "CROSS JOIN LATERAL("
+ "SELECT height, state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ATs.AT_address ");
if (minimumFinalHeight != null) {
sql.append("AND height >= ? ");
sql.append("AND ATStates.height >= ? ");
bindParams.add(minimumFinalHeight);
}
// AT_address then height so the compound primary key is used as an index
// Both must be the same direction also
sql.append("ORDER BY AT_address DESC, height DESC "
// Order by AT_address and height to use compound primary key as index
// Both must be the same direction (DESC) also
sql.append("ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
+ "LIMIT 1 "
+ ") AS FinalATStates "
+ "WHERE code_hash = ? ");
@ -337,7 +340,7 @@ public class HSQLDBATRepository implements ATRepository {
bindParams.add(rawExpectedValue);
}
sql.append(" ORDER BY height ");
sql.append(" ORDER BY FinalATStates.height ");
if (reverse != null && reverse)
sql.append("DESC");
@ -431,7 +434,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public void prepareForAtStateTrimming() throws DataException {
// Rebuild cache of latest, non-finished AT states that we can't trim
// Rebuild cache of latest AT states that we can't trim
String deleteSql = "DELETE FROM LatestATStates";
try {
this.repository.executeCheckedUpdate(deleteSql);
@ -463,13 +466,12 @@ public class HSQLDBATRepository implements ATRepository {
// We're often called so no need to trim all states in one go.
// Limit updates to reduce CPU and memory load.
String sql = "UPDATE ATStates SET state_data = NULL "
+ "WHERE state_data IS NOT NULL "
+ "AND height BETWEEN ? AND ? "
String sql = "DELETE FROM ATStatesData "
+ "WHERE height BETWEEN ? AND ? "
+ "AND NOT EXISTS("
+ "SELECT TRUE FROM LatestATStates "
+ "WHERE LatestATStates.AT_address = ATStates.AT_address "
+ "AND LatestATStates.height = ATStates.height"
+ "WHERE LatestATStates.AT_address = ATStatesData.AT_address "
+ "AND LatestATStates.height = ATStatesData.height"
+ ") "
+ "LIMIT ?";
@ -487,23 +489,44 @@ public class HSQLDBATRepository implements ATRepository {
if (atStateData.getStateHash() == null || atStateData.getHeight() == null)
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates");
HSQLDBSaver atStatesSaver = new HSQLDBSaver("ATStates");
saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("state_data", atStateData.getStateData()).bind("state_hash", atStateData.getStateHash())
atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("state_hash", atStateData.getStateHash())
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial());
try {
saveHelper.execute(this.repository);
atStatesSaver.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save AT state into repository", e);
}
if (atStateData.getStateData() != null) {
HSQLDBSaver atStatesDataSaver = new HSQLDBSaver("ATStatesData");
atStatesDataSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("state_data", atStateData.getStateData());
try {
atStatesDataSaver.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save AT state data into repository", e);
}
} else {
try {
this.repository.delete("ATStatesData", "AT_address = ? AND height = ?",
atStateData.getATAddress(), atStateData.getHeight());
} catch (SQLException e) {
throw new DataException("Unable to delete AT state data from repository", e);
}
}
}
@Override
public void delete(String atAddress, int height) throws DataException {
try {
this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height);
this.repository.delete("ATStatesData", "AT_address = ? AND height = ?", atAddress, height);
} catch (SQLException e) {
throw new DataException("Unable to delete AT state from repository", e);
}
@ -513,6 +536,7 @@ public class HSQLDBATRepository implements ATRepository {
public void deleteATStates(int height) throws DataException {
try {
this.repository.delete("ATStates", "height = ?", height);
this.repository.delete("ATStatesData", "height = ?", height);
} catch (SQLException e) {
throw new DataException("Unable to delete AT states from repository", e);
}

View File

@ -660,9 +660,10 @@ public class HSQLDBDatabaseUpdates {
break;
case 25:
// DISABLED: improved version in case 30!
// Remove excess created_when from ATStates
stmt.execute("ALTER TABLE ATStates DROP created_when");
stmt.execute("CREATE INDEX ATStateHeightIndex on ATStates (height)");
// stmt.execute("ALTER TABLE ATStates DROP created_when");
// stmt.execute("CREATE INDEX ATStateHeightIndex on ATStates (height)");
break;
case 26:
@ -691,6 +692,69 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CHECKPOINT");
break;
case 30:
// Split AT state data off to new table for better performance/management.
if (!"mem".equals(HSQLDBRepository.getDbPathname(connection.getMetaData().getURL()))) {
// First, backup node-local data in case user wants to avoid long reshape and use bootstrap instead
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'MintingAccounts.script'");
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'");
LOGGER.info("Exported sensitive/node-local data: minting keys and trade bot states");
LOGGER.info("If following reshape takes too long, use bootstrap and import node-local data using API's POST /admin/repository");
}
// Create new AT-states table without full state data
stmt.execute("CREATE TABLE ATStatesNew ("
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_hash ATStateHash NOT NULL, "
+ "fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, "
+ "PRIMARY KEY (AT_address, height), "
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
stmt.execute("SET TABLE ATStatesNew NEW SPACE");
stmt.execute("CHECKPOINT");
ResultSet resultSet = stmt.executeQuery("SELECT height FROM Blocks ORDER BY height DESC LIMIT 1");
final int blockchainHeight = resultSet.next() ? resultSet.getInt(1) : 0;
final int heightStep = 100;
LOGGER.info("Rebuilding AT state summaries in repository - this might take a while... (approx. 2 mins on high-spec)");
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
stmt.execute("INSERT INTO ATStatesNew ("
+ "SELECT AT_address, height, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
+ ")");
stmt.execute("COMMIT");
}
stmt.execute("CHECKPOINT");
LOGGER.info("Rebuilding AT states height index in repository - this might take about 3x longer...");
stmt.execute("CREATE INDEX ATStatesHeightIndex ON ATStatesNew (height)");
stmt.execute("CHECKPOINT");
stmt.execute("CREATE TABLE ATStatesData ("
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_data ATState NOT NULL, "
+ "PRIMARY KEY (height, AT_address), "
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
stmt.execute("SET TABLE ATStatesData NEW SPACE");
stmt.execute("CHECKPOINT");
LOGGER.info("Rebuilding AT state data in repository - this might take a while... (approx. 2 mins on high-spec)");
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
stmt.execute("INSERT INTO ATStatesData ("
+ "SELECT AT_address, height, state_data "
+ "FROM ATstates "
+ "WHERE state_data IS NOT NULL "
+ "AND height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
+ ")");
stmt.execute("COMMIT");
}
stmt.execute("CHECKPOINT");
stmt.execute("DROP TABLE ATStates");
stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates");
stmt.execute("CHECKPOINT");
break;
default:
// nothing to do
return false;

View File

@ -426,7 +426,7 @@ public class HSQLDBRepository implements Repository {
}
/** Returns DB pathname from passed connection URL. If memory DB, returns "mem". */
private static String getDbPathname(String connectionUrl) {
/*package*/ static String getDbPathname(String connectionUrl) {
Pattern pattern = Pattern.compile("hsqldb:(mem|file):(.*?)(;|$)");
Matcher matcher = pattern.matcher(connectionUrl);

View File

@ -0,0 +1,426 @@
package org.qortal.test.at;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.List;
import org.ciyam.at.CompilationException;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
public class AtRepositoryTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testGetATStateAtHeightWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
Integer testHeight = 8;
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetATStateAtHeightWithoutData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int maxHeight = 8;
Integer testHeight = maxHeight - 2;
// Trim AT state data
repository.getATRepository().prepareForAtStateTrimming();
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNull(atStateData.getStateData());
}
}
@Test
public void testGetLatestATStateWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight;
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetLatestATStatePostTrimming() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
int maxHeight = blockchainHeight + 100; // more than latest block height
Integer testHeight = blockchainHeight;
// Trim AT state data
repository.getATRepository().prepareForAtStateTrimming();
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
assertEquals(testHeight, atStateData.getHeight());
// We should always have the latest AT state data available
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetMatchingFinalATStatesWithoutDataValue() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight;
ATData atData = repository.getATRepository().fromATAddress(atAddress);
byte[] codeHash = atData.getCodeHash();
Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = null;
Long expectedValue = null;
Integer minimumFinalHeight = null;
Integer limit = null;
Integer offset = null;
Boolean reverse = null;
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
codeHash,
isFinished,
dataByteOffset,
expectedValue,
minimumFinalHeight,
limit, offset, reverse);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetMatchingFinalATStatesWithDataValue() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight;
ATData atData = repository.getATRepository().fromATAddress(atAddress);
byte[] codeHash = atData.getCodeHash();
Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = MachineState.HEADER_LENGTH + 0;
Long expectedValue = 0L;
Integer minimumFinalHeight = null;
Integer limit = null;
Integer offset = null;
Boolean reverse = null;
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
codeHash,
isFinished,
dataByteOffset,
expectedValue,
minimumFinalHeight,
limit, offset, reverse);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetBlockATStatesAtHeightWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
doDeploy(repository, deployer, creationBytes, fundingAmount);
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
Integer testHeight = 8;
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
// getBlockATStatesAtHeight never returns actual AT state data anyway
assertNull(atStateData.getStateData());
}
}
@Test
public void testGetBlockATStatesAtHeightWithoutData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
doDeploy(repository, deployer, creationBytes, fundingAmount);
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int maxHeight = 8;
Integer testHeight = maxHeight - 2;
// Trim AT state data
repository.getATRepository().prepareForAtStateTrimming();
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
// getBlockATStatesAtHeight never returns actual AT state data anyway
assertNull(atStateData.getStateData());
}
}
@Test
public void testSaveATStateWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight - 2;
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
repository.getATRepository().save(atStateData);
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testSaveATStateWithoutData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight - 2;
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
// Clear data
ATStateData newAtStateData = new ATStateData(atStateData.getATAddress(),
atStateData.getHeight(),
/*StateData*/ null,
atStateData.getStateHash(),
atStateData.getFees(),
atStateData.isInitial());
repository.getATRepository().save(newAtStateData);
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNull(atStateData.getStateData());
}
}
private byte[] buildSimpleAT() {
// Pretend we use 4 values in data segment
int addrCounter = 4;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "Test AT";
String description = "Test AT";
String atType = "Test";
String tags = "TEST";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
}