diff --git a/src/main/java/org/qora/api/resource/NamesResource.java b/src/main/java/org/qora/api/resource/NamesResource.java index 47aff78d..fde7429a 100644 --- a/src/main/java/org/qora/api/resource/NamesResource.java +++ b/src/main/java/org/qora/api/resource/NamesResource.java @@ -29,6 +29,7 @@ import org.qora.api.model.NameSummary; import org.qora.crypto.Crypto; import org.qora.data.naming.NameData; import org.qora.data.transaction.RegisterNameTransactionData; +import org.qora.data.transaction.UpdateNameTransactionData; import org.qora.repository.DataException; import org.qora.repository.Repository; import org.qora.repository.RepositoryManager; @@ -36,6 +37,7 @@ import org.qora.transaction.Transaction; import org.qora.transaction.Transaction.ValidationResult; import org.qora.transform.TransformationException; import org.qora.transform.transaction.RegisterNameTransactionTransformer; +import org.qora.transform.transaction.UpdateNameTransactionTransformer; import org.qora.utils.Base58; @Path("/names") @@ -153,7 +155,7 @@ public class NamesResource { } ) @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) - public String buildTransaction(RegisterNameTransactionData transactionData) { + public String registerName(RegisterNameTransactionData transactionData) { try (final Repository repository = RepositoryManager.getRepository()) { Transaction transaction = Transaction.fromData(repository, transactionData); @@ -170,4 +172,47 @@ public class NamesResource { } } + @POST + @Path("/update") + @Operation( + summary = "Build raw, unsigned, UPDATE_NAME transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = UpdateNameTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, UPDATE_NAME transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String updateName(UpdateNameTransactionData transactionData) { + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + ValidationResult result = transaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = UpdateNameTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + } \ No newline at end of file diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index daccd873..f08d67d6 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -543,8 +543,8 @@ public class Block { if (this.blockData.getGeneratorSignature() == null) throw new IllegalStateException("Cannot calculate transactions signature as block has no generator signature"); - // Already added? - if (this.transactions.contains(transactionData)) + // Already added? (Check using signature) + if (this.transactions.stream().anyMatch(transaction -> Arrays.equals(transaction.getTransactionData().getSignature(), transactionData.getSignature()))) return true; // Check there is space in block @@ -573,6 +573,47 @@ public class Block { return true; } + /** + * Remove a transaction from the block. + *
+ * Used when constructing a new block during forging. + *
+ * Requires block's {@code generator} being a {@code PrivateKeyAccount} so block's transactions signature can be recalculated. + * + * @param transactionData + * @throws IllegalStateException + * if block's {@code generator} is not a {@code PrivateKeyAccount}. + */ + public void deleteTransaction(TransactionData transactionData) { + // Can't add to transactions if we haven't loaded existing ones yet + if (this.transactions == null) + throw new IllegalStateException("Attempted to add transaction to partially loaded database Block"); + + if (!(this.generator instanceof PrivateKeyAccount)) + throw new IllegalStateException("Block's generator has no private key"); + + if (this.blockData.getGeneratorSignature() == null) + throw new IllegalStateException("Cannot calculate transactions signature as block has no generator signature"); + + // Attempt to remove from block (Check using signature) + boolean wasElementRemoved = this.transactions.removeIf(transaction -> Arrays.equals(transaction.getTransactionData().getSignature(), transactionData.getSignature())); + if (!wasElementRemoved) + // Wasn't there - nothing more to do + return; + + // Re-sort + this.transactions.sort(Transaction.getComparator()); + + // Update transaction count + this.blockData.setTransactionCount(this.blockData.getTransactionCount() - 1); + + // Update totalFees + this.blockData.setTotalFees(this.blockData.getTotalFees().subtract(transactionData.getFee())); + + // We've removed a transaction, so recalculate transactions signature + calcTransactionsSignature(); + } + /** * Recalculate block's generator signature. *
@@ -787,7 +828,7 @@ public class Block {
// NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid
Transaction.ValidationResult validationResult = transaction.isValid();
if (validationResult != Transaction.ValidationResult.OK) {
- LOGGER.error("Error during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": "
+ LOGGER.debug("Error during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": "
+ validationResult.name());
return ValidationResult.TRANSACTION_INVALID;
}
diff --git a/src/main/java/org/qora/block/BlockGenerator.java b/src/main/java/org/qora/block/BlockGenerator.java
index 4b71120d..54b757ba 100644
--- a/src/main/java/org/qora/block/BlockGenerator.java
+++ b/src/main/java/org/qora/block/BlockGenerator.java
@@ -147,9 +147,21 @@ public class BlockGenerator extends Thread {
repository.discardChanges();
// Attempt to add transactions until block is full, or we run out
- for (TransactionData transactionData : unconfirmedTransactions)
+ // If a transaction makes the block invalid then skip it and it'll either expire or be in next block.
+ for (TransactionData transactionData : unconfirmedTransactions) {
if (!newBlock.addTransaction(transactionData))
break;
+
+ // Sign to create block's signature
+ newBlock.sign();
+
+ // If newBlock is no longer valid then we can't use transaction
+ ValidationResult validationResult = newBlock.isValid();
+ if (validationResult != ValidationResult.OK) {
+ LOGGER.debug("Skipping invalid transaction " + Base58.encode(transactionData.getSignature()) + " during block generation");
+ newBlock.deleteTransaction(transactionData);
+ }
+ }
}
public void shutdown() {
diff --git a/src/main/java/org/qora/data/naming/NameData.java b/src/main/java/org/qora/data/naming/NameData.java
index c8dedf11..8bf3b075 100644
--- a/src/main/java/org/qora/data/naming/NameData.java
+++ b/src/main/java/org/qora/data/naming/NameData.java
@@ -10,7 +10,6 @@ import javax.xml.bind.annotation.XmlAccessorType;
public class NameData {
// Properties
- private byte[] registrantPublicKey;
private String owner;
private String name;
private String data;
@@ -26,9 +25,8 @@ public class NameData {
protected NameData() {
}
- public NameData(byte[] registrantPublicKey, String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale,
+ public NameData(String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale,
BigDecimal salePrice) {
- this.registrantPublicKey = registrantPublicKey;
this.owner = owner;
this.name = name;
this.data = data;
@@ -39,16 +37,12 @@ public class NameData {
this.salePrice = salePrice;
}
- public NameData(byte[] registrantPublicKey, String owner, String name, String data, long registered, byte[] reference) {
- this(registrantPublicKey, owner, name, data, registered, null, reference, false, null);
+ public NameData(String owner, String name, String data, long registered, byte[] reference) {
+ this(owner, name, data, registered, null, reference, false, null);
}
// Getters / setters
- public byte[] getRegistrantPublicKey() {
- return this.registrantPublicKey;
- }
-
public String getOwner() {
return this.owner;
}
diff --git a/src/main/java/org/qora/data/transaction/UpdateNameTransactionData.java b/src/main/java/org/qora/data/transaction/UpdateNameTransactionData.java
index 815a8bfa..f4bb0e06 100644
--- a/src/main/java/org/qora/data/transaction/UpdateNameTransactionData.java
+++ b/src/main/java/org/qora/data/transaction/UpdateNameTransactionData.java
@@ -4,6 +4,7 @@ import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlTransient;
import org.qora.transaction.Transaction.TransactionType;
@@ -15,16 +16,24 @@ import io.swagger.v3.oas.annotations.media.Schema;
public class UpdateNameTransactionData extends TransactionData {
// Properties
+ @Schema(description = "owner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
private byte[] ownerPublicKey;
+ @Schema(description = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
private String newOwner;
+ @Schema(description = "which name to update", example = "my-name")
private String name;
+ @Schema(description = "replacement simple name-related info in JSON format", example = "{ \"age\": 30 }")
private String newData;
+ // For internal use when orphaning
+ @XmlTransient
+ @Schema(hidden = true)
private byte[] nameReference;
// Constructors
// For JAX-RS
protected UpdateNameTransactionData() {
+ super(TransactionType.UPDATE_NAME);
}
public UpdateNameTransactionData(byte[] ownerPublicKey, String newOwner, String name, String newData, byte[] nameReference, BigDecimal fee, long timestamp,
diff --git a/src/main/java/org/qora/naming/Name.java b/src/main/java/org/qora/naming/Name.java
index 11903089..fb419a3a 100644
--- a/src/main/java/org/qora/naming/Name.java
+++ b/src/main/java/org/qora/naming/Name.java
@@ -33,7 +33,7 @@ public class Name {
*/
public Name(Repository repository, RegisterNameTransactionData registerNameTransactionData) {
this.repository = repository;
- this.nameData = new NameData(registerNameTransactionData.getRegistrantPublicKey(), registerNameTransactionData.getOwner(),
+ this.nameData = new NameData(registerNameTransactionData.getOwner(),
registerNameTransactionData.getName(), registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(),
registerNameTransactionData.getSignature());
}
diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
index b13f08d1..b9dd6401 100644
--- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
+++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
@@ -184,7 +184,7 @@ public class HSQLDBDatabaseUpdates {
case 6:
// Update Name Transactions
stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QoraPublicKey NOT NULL, name RegisteredName NOT NULL, "
- + "new_owner QoraAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature NOT NULL, "
+ + "new_owner QoraAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
@@ -203,7 +203,7 @@ public class HSQLDBDatabaseUpdates {
case 9:
// Buy Name Transactions
stmt.execute("CREATE TABLE BuyNameTransactions (signature Signature, buyer QoraPublicKey NOT NULL, name RegisteredName NOT NULL, "
- + "seller QoraAddress NOT NULL, amount QoraAmount NOT NULL, name_reference Signature NOT NULL, "
+ + "seller QoraAddress NOT NULL, amount QoraAmount NOT NULL, name_reference Signature, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
@@ -357,7 +357,7 @@ public class HSQLDBDatabaseUpdates {
case 26:
// Registered Names
stmt.execute(
- "CREATE TABLE Names (name RegisteredName, data VARCHAR(4000) NOT NULL, registrant QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, "
+ "CREATE TABLE Names (name RegisteredName, data VARCHAR(4000) NOT NULL, owner QoraAddress NOT NULL, "
+ "registered TIMESTAMP WITH TIME ZONE NOT NULL, updated TIMESTAMP WITH TIME ZONE, reference Signature, is_for_sale BOOLEAN NOT NULL, sale_price QoraAmount, "
+ "PRIMARY KEY (name))");
break;
@@ -389,6 +389,15 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE INDEX ATTransactionsIndex on ATTransactions (AT_address)");
break;
+ case 28:
+ // XXX TEMP fix until database rebuild
+ // Allow name_reference to be NULL while transaction is unconfirmed
+ stmt.execute("ALTER TABLE UpdateNameTransactions ALTER COLUMN name_reference SET NULL");
+ stmt.execute("ALTER TABLE BuyNameTransactions ALTER COLUMN name_reference SET NULL");
+ // Names.registrant shouldn't be there
+ stmt.execute("ALTER TABLE Names DROP COLUMN registrant");
+ break;
+
default:
// nothing to do
return false;
diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java
index bf183d49..b5dbb2e5 100644
--- a/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java
+++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java
@@ -23,24 +23,23 @@ public class HSQLDBNameRepository implements NameRepository {
@Override
public NameData fromName(String name) throws DataException {
try (ResultSet resultSet = this.repository
- .checkedExecute("SELECT registrant, owner, data, registered, updated, reference, is_for_sale, sale_price FROM Names WHERE name = ?", name)) {
+ .checkedExecute("SELECT owner, data, registered, updated, reference, is_for_sale, sale_price FROM Names WHERE name = ?", name)) {
if (resultSet == null)
return null;
- byte[] registrantPublicKey = resultSet.getBytes(1);
- String owner = resultSet.getString(2);
- String data = resultSet.getString(3);
- long registered = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
+ String owner = resultSet.getString(1);
+ String data = resultSet.getString(2);
+ long registered = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
// Special handling for possibly-NULL "updated" column
- Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC));
+ Timestamp updatedTimestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC));
Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime();
- byte[] reference = resultSet.getBytes(6);
- boolean isForSale = resultSet.getBoolean(7);
- BigDecimal salePrice = resultSet.getBigDecimal(8);
+ byte[] reference = resultSet.getBytes(5);
+ boolean isForSale = resultSet.getBoolean(6);
+ BigDecimal salePrice = resultSet.getBigDecimal(7);
- return new NameData(registrantPublicKey, owner, name, data, registered, updated, reference, isForSale, salePrice);
+ return new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice);
} catch (SQLException e) {
throw new DataException("Unable to fetch name info from repository", e);
}
@@ -60,26 +59,25 @@ public class HSQLDBNameRepository implements NameRepository {
List