Browse Source

Work on groups

Some dev/testing API calls are now turned off by default in production mode,
 see "restrictApi" settings entry, returning NON_PRODUCTION API error.

Corrections to how account's defaultGroupId works, removing "effective groupID"
 which overly complicated matters.
In relation to above, DEFAULT_GROUP (0) no longer exists and NO_GROUP(-1) now has
 the value 0 instead.
So transactions can no longer have txGroupId of DEFAULT_GROUP, which in turn
 required all the erroneous "effective groupID" code.

API call /addresses/{address} now supplies blockchain-wide defaultGroupId if
 account doesn't exist or if account's default not set and NO-GROUP not allowed.

API /transactions/pending now offloaded to repository instead of Java-based
 processing and filtering.

Transaction approval checks added to Block.isValid

Groups now have min/max approval block delays.
 Checks added to incoming unconfirmed, block generator, block.isValid, etc.

'needing approval' and 'meets approval threshold' now split into separate calls.

NB: settings.json no longer part of git repo
pull/67/head
catbref 6 years ago
parent
commit
86a35c3b71
  1. 6
      settings.json
  2. 1
      src/main/java/org/qora/api/ApiError.java
  3. 20
      src/main/java/org/qora/api/resource/AddressesResource.java
  4. 16
      src/main/java/org/qora/api/resource/AssetsResource.java
  5. 66
      src/main/java/org/qora/api/resource/GroupsResource.java
  6. 26
      src/main/java/org/qora/api/resource/NamesResource.java
  7. 8
      src/main/java/org/qora/api/resource/PaymentsResource.java
  8. 25
      src/main/java/org/qora/api/resource/TransactionsResource.java
  9. 42
      src/main/java/org/qora/api/resource/UtilsResource.java
  10. 6
      src/main/java/org/qora/at/QoraATAPI.java
  11. 5
      src/main/java/org/qora/block/Block.java
  12. 16
      src/main/java/org/qora/block/BlockChain.java
  13. 5
      src/main/java/org/qora/block/BlockGenerator.java
  14. 2
      src/main/java/org/qora/data/account/AccountData.java
  15. 18
      src/main/java/org/qora/data/group/GroupData.java
  16. 16
      src/main/java/org/qora/data/transaction/CreateGroupTransactionData.java
  17. 20
      src/main/java/org/qora/data/transaction/UpdateGroupTransactionData.java
  18. 23
      src/main/java/org/qora/group/Group.java
  19. 9
      src/main/java/org/qora/repository/BlockRepository.java
  20. 14
      src/main/java/org/qora/repository/TransactionRepository.java
  21. 12
      src/main/java/org/qora/repository/hsqldb/HSQLDBBlockRepository.java
  22. 10
      src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java
  23. 39
      src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java
  24. 15
      src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateGroupTransactionRepository.java
  25. 58
      src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
  26. 8
      src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBUpdateGroupTransactionRepository.java
  27. 12
      src/main/java/org/qora/settings/Settings.java
  28. 9
      src/main/java/org/qora/transaction/GroupApprovalTransaction.java
  29. 22
      src/main/java/org/qora/transaction/GroupInviteTransaction.java
  30. 16
      src/main/java/org/qora/transaction/JoinGroupTransaction.java
  31. 9
      src/main/java/org/qora/transaction/SetGroupTransaction.java
  32. 150
      src/main/java/org/qora/transaction/Transaction.java
  33. 15
      src/main/java/org/qora/transform/transaction/CreateGroupTransactionTransformer.java
  34. 10
      src/main/java/org/qora/transform/transaction/UpdateGroupTransactionTransformer.java
  35. 4
      src/main/java/org/qora/v1feeder.java
  36. 1
      src/main/resources/i18n/ApiError_en.properties
  37. 2
      src/test/java/org/qora/test/ATTests.java
  38. 2
      src/test/java/org/qora/test/SaveTests.java
  39. 36
      src/test/java/org/qora/test/TransactionTests.java

6
settings.json

@ -1,6 +0,0 @@
{
"rpcallowed": [
"::/0",
"0.0.0.0/0"
]
}

1
src/main/java/org/qora/api/ApiError.java

@ -13,6 +13,7 @@ public enum ApiError {
NOT_YET_RELEASED(3, 422),
UNAUTHORIZED(4, 403),
REPOSITORY_ISSUE(5, 500),
NON_PRODUCTION(6, 403),
// VALIDATION
INVALID_SIGNATURE(101, 400),

20
src/main/java/org/qora/api/resource/AddressesResource.java

@ -22,11 +22,14 @@ import org.qora.api.ApiErrors;
import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory;
import org.qora.asset.Asset;
import org.qora.block.BlockChain;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountData;
import org.qora.group.Group;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
import org.qora.transform.Transformer;
import org.qora.utils.Base58;
@ -55,7 +58,17 @@ public class AddressesResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getAccountRepository().getAccount(address);
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null)
accountData = new AccountData(address, null, null, BlockChain.getInstance().getDefaultGroupId());
// If Blockchain config doesn't allow NO_GROUP then change this to blockchain's default groupID
if (accountData.getDefaultGroupId() == Group.NO_GROUP && !BlockChain.getInstance().getGrouplessAllowed())
accountData.setDefaultGroupId(BlockChain.getInstance().getDefaultGroupId());
return accountData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@ -227,8 +240,11 @@ public class AddressesResource {
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.NON_PRODUCTION, ApiError.REPOSITORY_ISSUE})
public String fromPublicKey(@PathParam("publickey") String publicKey58) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
// Decode public key
byte[] publicKey;
try {

16
src/main/java/org/qora/api/resource/AssetsResource.java

@ -43,6 +43,7 @@ import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
@ -554,9 +555,12 @@ public class AssetsResource {
}
)
@ApiErrors({
ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID
ApiError.NON_PRODUCTION, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID
})
public String cancelOrder(CancelAssetOrderTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -599,9 +603,12 @@ public class AssetsResource {
}
)
@ApiErrors({
ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID
ApiError.NON_PRODUCTION, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID
})
public String issueAsset(IssueAssetTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -644,9 +651,12 @@ public class AssetsResource {
}
)
@ApiErrors({
ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID
ApiError.NON_PRODUCTION, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID
})
public String createOrder(CreateAssetOrderTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);

66
src/main/java/org/qora/api/resource/GroupsResource.java

@ -51,6 +51,7 @@ import org.qora.data.transaction.UpdateGroupTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
@ -264,8 +265,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String createGroup(CreateGroupTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -307,8 +311,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String updateGroup(UpdateGroupTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -350,8 +357,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String addGroupAdmin(AddGroupAdminTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -393,8 +403,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String removeGroupAdmin(RemoveGroupAdminTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -436,8 +449,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String groupBan(GroupBanTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -479,8 +495,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String cancelGroupBan(CancelGroupBanTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -522,8 +541,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String groupKick(GroupKickTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -565,8 +587,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String groupInvite(GroupInviteTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -608,8 +633,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String cancelGroupInvite(CancelGroupInviteTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -651,8 +679,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String joinGroup(JoinGroupTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -694,8 +725,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String leaveGroup(LeaveGroupTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -829,8 +863,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String groupApproval(GroupApprovalTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -872,8 +909,11 @@ public class GroupsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String setGroup(SetGroupTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);

26
src/main/java/org/qora/api/resource/NamesResource.java

@ -36,6 +36,7 @@ import org.qora.data.transaction.UpdateNameTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
@ -158,8 +159,11 @@ public class NamesResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String registerName(RegisterNameTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -201,8 +205,11 @@ public class NamesResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String updateName(UpdateNameTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -244,8 +251,11 @@ public class NamesResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String sellName(SellNameTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -287,8 +297,11 @@ public class NamesResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String cancelSellName(CancelSellNameTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
@ -330,8 +343,11 @@ public class NamesResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String buyName(BuyNameTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);

8
src/main/java/org/qora/api/resource/PaymentsResource.java

@ -21,6 +21,7 @@ import org.qora.data.transaction.PaymentTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
@ -60,8 +61,11 @@ public class PaymentsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String buildTransaction(PaymentTransactionData transactionData) {
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String makePayment(PaymentTransactionData transactionData) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);

25
src/main/java/org/qora/api/resource/TransactionsResource.java

@ -34,6 +34,7 @@ import org.qora.globalization.Translator;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transaction.Transaction.ValidationResult;
@ -246,7 +247,7 @@ public class TransactionsResource {
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public List<TransactionData> getPendingTransactions(@QueryParam("groupId") Integer groupId, @Parameter(
public List<TransactionData> getPendingTransactions(@QueryParam("txGroupId") Integer txGroupId, @Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
@ -254,22 +255,7 @@ public class TransactionsResource {
ref = "reverse"
) @QueryParam("reverse") Boolean reverse) {
try (final Repository repository = RepositoryManager.getRepository()) {
List<TransactionData> transactions = repository.getTransactionRepository().getUnconfirmedTransactions(null, null, reverse);
transactions.removeIf(transactionData -> {
if (groupId != null && groupId != transactionData.getTxGroupId())
return true;
try {
return !Transaction.fromData(repository, transactionData).needsGroupApproval();
} catch (DataException e) {
return true;
}
});
// Results slicing
return transactions;
return repository.getTransactionRepository().getPendingTransactions(txGroupId, limit, offset, reverse);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@ -366,9 +352,12 @@ public class TransactionsResource {
}
)
@ApiErrors({
ApiError.INVALID_PRIVATE_KEY, ApiError.TRANSFORMATION_ERROR
ApiError.NON_PRODUCTION, ApiError.INVALID_PRIVATE_KEY, ApiError.TRANSFORMATION_ERROR
})
public String signTransaction(SimpleTransactionSignRequest signRequest) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
if (signRequest.transactionBytes.length == 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.JSON);

42
src/main/java/org/qora/api/resource/UtilsResource.java

@ -29,6 +29,7 @@ import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiExceptionFactory;
import org.qora.crypto.Crypto;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transform.transaction.TransactionTransformer;
import org.qora.transform.transaction.TransactionTransformer.Transformation;
@ -76,8 +77,11 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA})
public String fromBase64(String base64) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try {
return HashCode.fromBytes(Base64.getDecoder().decode(base64.trim())).toString();
} catch (IllegalArgumentException e) {
@ -109,8 +113,11 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA})
public String base64from58(String base58) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try {
return HashCode.fromBytes(Base58.decode(base58.trim())).toString();
} catch (NumberFormatException e) {
@ -133,7 +140,11 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION})
public String toBase64(@PathParam("hex") String hex) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
return Base64.getEncoder().encodeToString(HashCode.fromString(hex).asBytes());
}
@ -152,7 +163,11 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION})
public String toBase58(@PathParam("hex") String hex) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
return Base58.encode(HashCode.fromString(hex).asBytes());
}
@ -173,7 +188,11 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION})
public String random(@QueryParam("length") Integer length) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
if (length == null)
length = 32;
@ -200,8 +219,11 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA})
public String getMnemonic(@QueryParam("entropy") String suppliedEntropy) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
/*
* BIP39 word lists have 2048 entries so can be represented by 11 bits.
* UUID (128bits) and another 4 bits gives 132 bits.
@ -266,7 +288,11 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION})
public String fromMnemonic(String mnemonic) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
if (mnemonic.isEmpty())
return "false";
@ -308,8 +334,11 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA})
public String privateKey(@PathParam("entropy") String entropy58) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
byte[] entropy;
try {
entropy = Base58.decode(entropy58);
@ -341,8 +370,11 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA})
public String publicKey(@PathParam("privateKey") String privateKey58) {
if (Settings.getInstance().isRestrictedApi())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
byte[] privateKey;
try {
privateKey = Base58.decode(privateKey58);

6
src/main/java/org/qora/at/QoraATAPI.java

@ -268,7 +268,7 @@ public class QoraATAPI extends API {
byte[] reference = this.getLastReference();
BigDecimal amount = BigDecimal.valueOf(unscaledAmount, 8);
ATTransactionData atTransactionData = new ATTransactionData(timestamp, Group.DEFAULT_GROUP, reference, this.atData.getATAddress(),
ATTransactionData atTransactionData = new ATTransactionData(timestamp, Group.NO_GROUP, reference, this.atData.getATAddress(),
recipient.getAddress(), amount, this.atData.getAssetId(), new byte[0], BigDecimal.ZERO.setScale(8));
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
@ -286,7 +286,7 @@ public class QoraATAPI extends API {
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
ATTransactionData atTransactionData = new ATTransactionData(timestamp, Group.DEFAULT_GROUP, reference,
ATTransactionData atTransactionData = new ATTransactionData(timestamp, Group.NO_GROUP, reference,
this.atData.getATAddress(), recipient.getAddress(), BigDecimal.ZERO, this.atData.getAssetId(), message, BigDecimal.ZERO.setScale(8));
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
@ -312,7 +312,7 @@ public class QoraATAPI extends API {
byte[] reference = this.getLastReference();
BigDecimal amount = BigDecimal.valueOf(finalBalance, 8);
ATTransactionData atTransactionData = new ATTransactionData(timestamp, Group.DEFAULT_GROUP, reference, this.atData.getATAddress(),
ATTransactionData atTransactionData = new ATTransactionData(timestamp, Group.NO_GROUP, reference, this.atData.getATAddress(),
creator.getAddress(), amount, this.atData.getAssetId(), new byte[0], BigDecimal.ZERO.setScale(8));
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);

5
src/main/java/org/qora/block/Block.java

@ -83,6 +83,7 @@ public class Block {
TRANSACTION_INVALID(52),
TRANSACTION_PROCESSING_FAILED(53),
TRANSACTION_ALREADY_PROCESSED(54),
TRANSACTION_NEEDS_APPROVAL(55),
AT_STATES_MISMATCH(61);
public final int value;
@ -834,6 +835,10 @@ public class Block {
if (this.repository.getTransactionRepository().isConfirmed(transaction.getTransactionData().getSignature()))
return ValidationResult.TRANSACTION_ALREADY_PROCESSED;
// Check transaction doesn't still need approval
if (transaction.needsGroupApproval() && !transaction.meetsGroupApprovalThreshold())
return ValidationResult.TRANSACTION_NEEDS_APPROVAL;
// Check transaction is even valid
// NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid
Transaction.ValidationResult validationResult = transaction.isValid();

16
src/main/java/org/qora/block/BlockChain.java

@ -36,11 +36,12 @@ public class BlockChain {
private static BlockChain instance = null;
// Properties
private boolean isTestNet;
private BigDecimal unitFee;
private BigDecimal maxBytesPerUnitFee;
private BigDecimal minFeePerByte;
/** Maximum coin supply. */
private BigDecimal maxBalance;;
private BigDecimal maxBalance;
/** Number of blocks between recalculating block's generating balance. */
private int blockDifficultyInterval;
/** Minimum target time between blocks, in seconds. */
@ -51,7 +52,7 @@ public class BlockChain {
private long blockTimestampMargin;
/** Whether transactions with txGroupId of NO_GROUP are allowed */
private boolean grouplessAllowed;
/** Default groupID when txGroupID and account's default groupID are both zero */
/** Default groupID when account's default groupID isn't set */
private int defaultGroupId = Group.NO_GROUP;
/** Map of which blockchain features are enabled when (height/timestamp) */
private Map<String, Map<FeatureValueType, Long>> featureTriggers;
@ -74,6 +75,10 @@ public class BlockChain {
// Getters / setters
public boolean getIsTestNet() {
return this.isTestNet;
}
public BigDecimal getUnitFee() {
return this.unitFee;
}
@ -190,11 +195,15 @@ public class BlockChain {
// If groupless is not allowed the defaultGroupId needs to be set
// XXX we could also check groupID exists, or at least created in genesis block, or in blockchain config
if (!grouplessAllowed && (defaultGroupId == null || defaultGroupId == Group.DEFAULT_GROUP || defaultGroupId == Group.NO_GROUP)) {
if (!grouplessAllowed && (defaultGroupId == null || defaultGroupId == Group.NO_GROUP)) {
LOGGER.error("defaultGroupId must be set to valid groupID in blockchain config if groupless transactions are not allowed");
throw new RuntimeException("defaultGroupId must be set to valid groupID in blockchain config if groupless transactions are not allowed");
}
boolean isTestNet = true;
if (json.containsKey("isTestNet"))
isTestNet = (Boolean) Settings.getTypedJson(json, "isTestNet", Boolean.class);
BigDecimal unitFee = Settings.getJsonBigDecimal(json, "unitFee");
long maxBytesPerUnitFee = (Long) Settings.getTypedJson(json, "maxBytesPerUnitFee", Long.class);
BigDecimal maxBalance = Settings.getJsonBigDecimal(json, "coinSupply");
@ -229,6 +238,7 @@ public class BlockChain {
}
instance = new BlockChain();
instance.isTestNet = isTestNet;
instance.unitFee = unitFee;
instance.maxBytesPerUnitFee = BigDecimal.valueOf(maxBytesPerUnitFee).setScale(8);
instance.minFeePerByte = unitFee.divide(instance.maxBytesPerUnitFee, MathContext.DECIMAL32);

5
src/main/java/org/qora/block/BlockGenerator.java

@ -169,16 +169,13 @@ public class BlockGenerator extends Thread {
}
// Ignore transactions that have not met group-admin approval threshold
if (transaction.needsGroupApproval()) {
if (transaction.needsGroupApproval() && !transaction.meetsGroupApprovalThreshold()) {
unconfirmedTransactions.remove(i);
--i;
continue;
}
}
// Discard any repository changes used to aid transaction validity checks
// repository.discardChanges(); // XXX possibly not needed any more thanks to savepoints?
// Attempt to add transactions until block is full, or we run out
// If a transaction makes the block invalid then skip it and it'll either expire or be in next block.
for (TransactionData transactionData : unconfirmedTransactions) {

2
src/main/java/org/qora/data/account/AccountData.java

@ -29,7 +29,7 @@ public class AccountData {
}
public AccountData(String address) {
this(address, null, null, Group.DEFAULT_GROUP);
this(address, null, null, Group.NO_GROUP);
}
// Getters/Setters

18
src/main/java/org/qora/data/group/GroupData.java

@ -21,6 +21,8 @@ public class GroupData {
private Long updated;
private boolean isOpen;
private ApprovalThreshold approvalThreshold;
private int minimumBlockDelay;
private int maximumBlockDelay;
/** Reference to CREATE_GROUP or UPDATE_GROUP transaction, used to rebuild group during orphaning. */
// No need to ever expose this via API
@XmlTransient
@ -34,7 +36,7 @@ public class GroupData {
}
/** Constructs new GroupData with nullable groupId and nullable updated [timestamp] */
public GroupData(Integer groupId, String owner, String name, String description, long created, Long updated, boolean isOpen, ApprovalThreshold approvalThreshold, byte[] reference) {
public GroupData(Integer groupId, String owner, String name, String description, long created, Long updated, boolean isOpen, ApprovalThreshold approvalThreshold, int minBlockDelay, int maxBlockDelay, byte[] reference) {
this.groupId = groupId;
this.owner = owner;
this.groupName = name;
@ -44,11 +46,13 @@ public class GroupData {
this.isOpen = isOpen;
this.approvalThreshold = approvalThreshold;
this.reference = reference;
this.minimumBlockDelay = minBlockDelay;
this.maximumBlockDelay = maxBlockDelay;
}
/** Constructs new GroupData with unassigned groupId */
public GroupData(String owner, String name, String description, long created, boolean isOpen, ApprovalThreshold approvalThreshold, byte[] reference) {
this(null, owner, name, description, created, null, isOpen, approvalThreshold, reference);
public GroupData(String owner, String name, String description, long created, boolean isOpen, ApprovalThreshold approvalThreshold, int minBlockDelay, int maxBlockDelay, byte[] reference) {
this(null, owner, name, description, created, null, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference);
}
// Getters / setters
@ -117,4 +121,12 @@ public class GroupData {
this.approvalThreshold = approvalThreshold;
}
public int getMinimumBlockDelay() {
return this.minimumBlockDelay;
}
public int getMaximumBlockDelay() {
return this.maximumBlockDelay;
}
}

16
src/main/java/org/qora/data/transaction/CreateGroupTransactionData.java

@ -52,6 +52,10 @@ public class CreateGroupTransactionData extends TransactionData {
description = "how many group admins are required to approve group member transactions"
)
private ApprovalThreshold approvalThreshold;
@Schema(description = "minimum block delay before approval takes effect")
private int minimumBlockDelay;
@Schema(description = "maximum block delay before which transaction approval must be reached")
private int maximumBlockDelay;
// Constructors
@ -61,7 +65,7 @@ public class CreateGroupTransactionData extends TransactionData {
}
public CreateGroupTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, String owner, String groupName, String description,
boolean isOpen, ApprovalThreshold approvalThreshold, Integer groupId, BigDecimal fee, byte[] signature) {
boolean isOpen, ApprovalThreshold approvalThreshold, int minimumBlockDelay, int maximumBlockDelay, Integer groupId, BigDecimal fee, byte[] signature) {
super(TransactionType.CREATE_GROUP, timestamp, txGroupId, reference, creatorPublicKey, fee, signature);
this.creatorPublicKey = creatorPublicKey;
@ -70,6 +74,8 @@ public class CreateGroupTransactionData extends TransactionData {
this.description = description;
this.isOpen = isOpen;
this.approvalThreshold = approvalThreshold;
this.minimumBlockDelay = minimumBlockDelay;
this.maximumBlockDelay = maximumBlockDelay;
this.groupId = groupId;
}
@ -95,6 +101,14 @@ public class CreateGroupTransactionData extends TransactionData {
return this.approvalThreshold;
}
public int getMinimumBlockDelay() {
return this.minimumBlockDelay;
}
public int getMaximumBlockDelay() {
return this.maximumBlockDelay;
}
public Integer getGroupId() {
return this.groupId;
}

20
src/main/java/org/qora/data/transaction/UpdateGroupTransactionData.java

@ -51,6 +51,10 @@ public class UpdateGroupTransactionData extends TransactionData {
description = "new group member transaction approval threshold"
)
private ApprovalThreshold newApprovalThreshold;
@Schema(description = "new minimum block delay before approval takes effect")
private int newMinimumBlockDelay;
@Schema(description = "new maximum block delay before which transaction approval must be reached")
private int newMaximumBlockDelay;
/** Reference to CREATE_GROUP or UPDATE_GROUP transaction, used to rebuild group during orphaning. */
// For internal use when orphaning
@XmlTransient
@ -71,7 +75,7 @@ public class UpdateGroupTransactionData extends TransactionData {
}
public UpdateGroupTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] ownerPublicKey, int groupId,
String newOwner, String newDescription, boolean newIsOpen, ApprovalThreshold newApprovalThreshold, byte[] groupReference, BigDecimal fee, byte[] signature) {
String newOwner, String newDescription, boolean newIsOpen, ApprovalThreshold newApprovalThreshold, int newMinimumBlockDelay, int newMaximumBlockDelay, byte[] groupReference, BigDecimal fee, byte[] signature) {
super(TransactionType.UPDATE_GROUP, timestamp, txGroupId, reference, ownerPublicKey, fee, signature);
this.ownerPublicKey = ownerPublicKey;
@ -80,13 +84,15 @@ public class UpdateGroupTransactionData extends TransactionData {
this.newDescription = newDescription;
this.newIsOpen = newIsOpen;
this.newApprovalThreshold = newApprovalThreshold;
this.newMinimumBlockDelay = newMinimumBlockDelay;
this.newMaximumBlockDelay = newMaximumBlockDelay;
this.groupReference = groupReference;
}
/** Constructor typically used after deserialization */
public UpdateGroupTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] ownerPublicKey, int groupId,
String newOwner, String newDescription, boolean newIsOpen, ApprovalThreshold newApprovalThreshold, BigDecimal fee, byte[] signature) {
this(timestamp, txGroupId, reference, ownerPublicKey, groupId, newOwner, newDescription, newIsOpen, newApprovalThreshold, null, fee, signature);
String newOwner, String newDescription, boolean newIsOpen, ApprovalThreshold newApprovalThreshold, int newMinimumBlockDelay, int newMaximumBlockDelay, BigDecimal fee, byte[] signature) {
this(timestamp, txGroupId, reference, ownerPublicKey, groupId, newOwner, newDescription, newIsOpen, newApprovalThreshold, newMinimumBlockDelay, newMaximumBlockDelay, null, fee, signature);
}
// Getters / setters
@ -115,6 +121,14 @@ public class UpdateGroupTransactionData extends TransactionData {
return this.newApprovalThreshold;
}
public int getNewMinimumBlockDelay() {
return this.newMinimumBlockDelay;
}
public int getNewMaximumBlockDelay() {
return this.newMaximumBlockDelay;
}
public byte[] getGroupReference() {
return this.groupReference;
}

23
src/main/java/org/qora/group/Group.java

@ -46,7 +46,8 @@ public class Group {
public final int value;
public final boolean isPercentage;
private final static Map<Integer, ApprovalThreshold> map = stream(ApprovalThreshold.values()).collect(toMap(threshold -> threshold.value, threshold -> threshold));
private final static Map<Integer, ApprovalThreshold> map = stream(ApprovalThreshold.values())
.collect(toMap(threshold -> threshold.value, threshold -> threshold));
ApprovalThreshold(int value, boolean isPercentage) {
this.value = value;
@ -65,23 +66,25 @@ public class Group {
}
/**
* Returns whether transaction need approval.
* Returns whether transaction meets approval threshold.
*
* @param repository
* @param txGroupId transaction's groupID
* @param signature transaction's signature
* @param txGroupId
* transaction's groupID
* @param signature
* transaction's signature
* @return true if approval still needed, false if transaction can be included in block
* @throws DataException
*/
public boolean needsApproval(Repository repository, int txGroupId, byte[] signature) throws DataException {
public boolean meetsApprovalThreshold(Repository repository, int txGroupId, byte[] signature) throws DataException {
// Fetch total number of admins in group
final int totalAdmins = repository.getGroupRepository().countGroupAdmins(txGroupId);
// Fetch total number of approvals for signature
// NOT simply number of GROUP_APPROVE transactions as some may be rejecting transaction, or changed opinions
final int currentApprovals = repository.getTransactionRepository().countTransactionApprovals(txGroupId, signature);
return !meetsTheshold(currentApprovals, totalAdmins);
return meetsTheshold(currentApprovals, totalAdmins);
}
}
@ -91,8 +94,7 @@ public class Group {
private GroupData groupData;
// Useful constants
public static final int NO_GROUP = -1;
public static final int DEFAULT_GROUP = 0;
public static final int NO_GROUP = 0;
public static final int MAX_NAME_SIZE = 32;
public static final int MAX_DESCRIPTION_SIZE = 128;
@ -113,7 +115,8 @@ public class Group {
this.groupData = new GroupData(createGroupTransactionData.getOwner(), createGroupTransactionData.getGroupName(),
createGroupTransactionData.getDescription(), createGroupTransactionData.getTimestamp(), createGroupTransactionData.getIsOpen(),
createGroupTransactionData.getApprovalThreshold(), createGroupTransactionData.getSignature());
createGroupTransactionData.getApprovalThreshold(), createGroupTransactionData.getMinimumBlockDelay(),
createGroupTransactionData.getMaximumBlockDelay(), createGroupTransactionData.getSignature());
}
/**

9
src/main/java/org/qora/repository/BlockRepository.java

@ -44,6 +44,15 @@ public interface BlockRepository {
*/
public int getHeightFromSignature(byte[] signature) throws DataException;
/**
* Return height of block with timestamp just before passed timestamp.
*
* @param timestamp
* @return height, or 0 if not found in blockchain.
* @throws DataException
*/
public int getHeightFromTimestamp(long timestamp) throws DataException;
/**
* Return highest block height from repository.
*

14
src/main/java/org/qora/repository/TransactionRepository.java

@ -47,6 +47,20 @@ public interface TransactionRepository {
public List<TransactionData> getAssetTransactions(int assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
throws DataException;
/**
* Returns list of transactions pending approval, with optional txGgroupId filtering.
* <p>
* This is typically called by the API.
*
* @param txGroupId
* @param limit
* @param offset
* @param reverse
* @return list of transactions, or empty if none.
* @throws DataException
*/
public List<TransactionData> getPendingTransactions(Integer txGroupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
/** Returns number of approvals for transaction with given signature. */
public int countTransactionApprovals(int txGroupId, byte[] signature) throws DataException;

12
src/main/java/org/qora/repository/hsqldb/HSQLDBBlockRepository.java

@ -90,6 +90,18 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
@Override
public int getHeightFromTimestamp(long timestamp) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT MAX(height) FROM Blocks WHERE generation <= ?", new Timestamp(timestamp))) {
if (resultSet == null)
return 0;
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Error obtaining block height from repository", e);
}
}
@Override
public int getBlockchainHeight() throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT MAX(height) FROM Blocks LIMIT 1")) {

10
src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java

@ -558,6 +558,16 @@ public class HSQLDBDatabaseUpdates {
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
case 35:
// Group-based transaction approval min/max block delay
stmt.execute("ALTER TABLE Groups ADD COLUMN min_block_delay INT NOT NULL DEFAULT 0 BEFORE reference");
stmt.execute("ALTER TABLE Groups ADD COLUMN max_block_delay INT NOT NULL DEFAULT 1440 BEFORE reference");
stmt.execute("ALTER TABLE CreateGroupTransactions ADD COLUMN min_block_delay INT NOT NULL DEFAULT 0 BEFORE group_id");
stmt.execute("ALTER TABLE CreateGroupTransactions ADD COLUMN max_block_delay INT NOT NULL DEFAULT 1440 BEFORE group_id");
stmt.execute("ALTER TABLE UpdateGroupTransactions ADD COLUMN new_min_block_delay INT NOT NULL DEFAULT 0 BEFORE group_reference");
stmt.execute("ALTER TABLE UpdateGroupTransactions ADD COLUMN new_max_block_delay INT NOT NULL DEFAULT 1440 BEFORE group_reference");
break;
default:
// nothing to do
return false;

39
src/main/java/org/qora/repository/hsqldb/HSQLDBGroupRepository.java

@ -30,7 +30,7 @@ public class HSQLDBGroupRepository implements GroupRepository {
@Override
public GroupData fromGroupId(int groupId) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT group_name, owner, description, created, updated, reference, is_open, approval_threshold FROM Groups WHERE group_id = ?", groupId)) {
.checkedExecute("SELECT group_name, owner, description, created, updated, reference, is_open, approval_threshold, min_block_delay, max_block_delay FROM Groups WHERE group_id = ?", groupId)) {
if (resultSet == null)
return null;
@ -48,7 +48,10 @@ public class HSQLDBGroupRepository implements GroupRepository {
ApprovalThreshold approvalThreshold = ApprovalThreshold.valueOf(resultSet.getInt(8));
return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, reference);
int minBlockDelay = resultSet.getInt(9);
int maxBlockDelay = resultSet.getInt(10);
return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference);
} catch (SQLException e) {
throw new DataException("Unable to fetch group info from repository", e);
}
@ -57,7 +60,7 @@ public class HSQLDBGroupRepository implements GroupRepository {
@Override
public GroupData fromGroupName(String groupName) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT group_id, owner, description, created, updated, reference, is_open, approval_threshold FROM Groups WHERE group_name = ?", groupName)) {
.checkedExecute("SELECT group_id, owner, description, created, updated, reference, is_open, approval_threshold, min_block_delay, max_block_delay FROM Groups WHERE group_name = ?", groupName)) {
if (resultSet == null)
return null;
@ -75,7 +78,10 @@ public class HSQLDBGroupRepository implements GroupRepository {
ApprovalThreshold approvalThreshold = ApprovalThreshold.valueOf(resultSet.getInt(8));
return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, reference);
int minBlockDelay = resultSet.getInt(9);
int maxBlockDelay = resultSet.getInt(10);
return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference);
} catch (SQLException e) {
throw new DataException("Unable to fetch group info from repository", e);
}
@ -101,7 +107,7 @@ public class HSQLDBGroupRepository implements GroupRepository {
@Override
public List<GroupData> getAllGroups(Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT group_id, owner, group_name, description, created, updated, reference, is_open, approval_threshold FROM Groups ORDER BY group_name";
String sql = "SELECT group_id, owner, group_name, description, created, updated, reference, is_open, approval_threshold, min_block_delay, max_block_delay FROM Groups ORDER BY group_name";
if (reverse != null && reverse)
sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
@ -128,7 +134,10 @@ public class HSQLDBGroupRepository implements GroupRepository {
ApprovalThreshold approvalThreshold = ApprovalThreshold.valueOf(resultSet.getInt(9));
groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, reference));
int minBlockDelay = resultSet.getInt(10);
int maxBlockDelay = resultSet.getInt(11);
groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference));
} while (resultSet.next());
return groups;
@ -139,7 +148,7 @@ public class HSQLDBGroupRepository implements GroupRepository {
@Override
public List<GroupData> getGroupsByOwner(String owner, Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT group_id, group_name, description, created, updated, reference, is_open, approval_threshold FROM Groups WHERE owner = ? ORDER BY group_name";
String sql = "SELECT group_id, group_name, description, created, updated, reference, is_open, approval_threshold, min_block_delay, max_block_delay FROM Groups WHERE owner = ? ORDER BY group_name";
if (reverse != null && reverse)
sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
@ -165,7 +174,10 @@ public class HSQLDBGroupRepository implements GroupRepository {
ApprovalThreshold approvalThreshold = ApprovalThreshold.valueOf(resultSet.getInt(8));
groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, reference));
int minBlockDelay = resultSet.getInt(9);
int maxBlockDelay = resultSet.getInt(10);
groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference));
} while (resultSet.next());
return groups;
@ -176,7 +188,8 @@ public class HSQLDBGroupRepository implements GroupRepository {
@Override
public List<GroupData> getGroupsWithMember(String member, Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT group_id, owner, group_name, description, created, updated, reference, is_open, approval_threshold FROM Groups JOIN GroupMembers USING (group_id) WHERE address = ? ORDER BY group_name";
String sql = "SELECT group_id, owner, group_name, description, created, updated, reference, is_open, approval_threshold min_block_delay, max_block_delay FROM Groups "
+ "JOIN GroupMembers USING (group_id) WHERE address = ? ORDER BY group_name";
if (reverse != null && reverse)
sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
@ -203,7 +216,10 @@ public class HSQLDBGroupRepository implements GroupRepository {
ApprovalThreshold approvalThreshold = ApprovalThreshold.valueOf(resultSet.getInt(9));
groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, reference));
int minBlockDelay = resultSet.getInt(10);
int maxBlockDelay = resultSet.getInt(11);
groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference));
} while (resultSet.next());
return groups;
@ -222,7 +238,8 @@ public class HSQLDBGroupRepository implements GroupRepository {
saveHelper.bind("group_id", groupData.getGroupId()).bind("owner", groupData.getOwner()).bind("group_name", groupData.getGroupName())
.bind("description", groupData.getDescription()).bind("created", new Timestamp(groupData.getCreated())).bind("updated", updatedTimestamp)
.bind("reference", groupData.getReference()).bind("is_open", groupData.getIsOpen()).bind("approval_threshold", groupData.getApprovalThreshold().value);
.bind("reference", groupData.getReference()).bind("is_open", groupData.getIsOpen()).bind("approval_threshold", groupData.getApprovalThreshold().value)
.bind("min_block_delay", groupData.getMinimumBlockDelay()).bind("max_block_delay", groupData.getMaximumBlockDelay());
try {
saveHelper.execute(this.repository);

15
src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateGroupTransactionRepository.java

@ -18,8 +18,9 @@ public class HSQLDBCreateGroupTransactionRepository extends HSQLDBTransactionRep
}
TransactionData fromBase(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, BigDecimal fee, byte[] signature) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT owner, group_name, description, is_open, approval_threshold, group_id FROM CreateGroupTransactions WHERE signature = ?", signature)) {
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT owner, group_name, description, is_open, approval_threshold, min_block_delay, max_block_delay, group_id FROM CreateGroupTransactions WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
@ -30,12 +31,15 @@ public class HSQLDBCreateGroupTransactionRepository extends HSQLDBTransactionRep
ApprovalThreshold approvalThreshold = ApprovalThreshold.valueOf(resultSet.getInt(5));
Integer groupId = resultSet.getInt(6);
int minBlockDelay = resultSet.getInt(6);
int maxBlockDelay = resultSet.getInt(7);
Integer groupId = resultSet.getInt(8);
if (resultSet.wasNull())
groupId = null;
return new CreateGroupTransactionData(timestamp, txGroupId, reference, creatorPublicKey, owner, groupName, description, isOpen, approvalThreshold,
groupId, fee, signature);
minBlockDelay, maxBlockDelay, groupId, fee, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch create group transaction from repository", e);
}
@ -51,7 +55,8 @@ public class HSQLDBCreateGroupTransactionRepository extends HSQLDBTransactionRep
.bind("owner", createGroupTransactionData.getOwner()).bind("group_name", createGroupTransactionData.getGroupName())
.bind("description", createGroupTransactionData.getDescription()).bind("is_open", createGroupTransactionData.getIsOpen())
.bind("approval_threshold", createGroupTransactionData.getApprovalThreshold().value)
.bind("group_id", createGroupTransactionData.getGroupId());
.bind("min_block_delay", createGroupTransactionData.getMinimumBlockDelay())
.bind("max_block_delay", createGroupTransactionData.getMaximumBlockDelay()).bind("group_id", createGroupTransactionData.getGroupId());
try {
saveHelper.execute(this.repository);

58
src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java

@ -9,6 +9,7 @@ import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import static java.util.Arrays.stream;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Collectors;
@ -19,10 +20,12 @@ import org.qora.api.resource.TransactionsResource.ConfirmationStatus;
import org.qora.data.PaymentData;
import org.qora.data.transaction.GroupApprovalTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.group.Group;
import org.qora.repository.DataException;
import org.qora.repository.TransactionRepository;
import org.qora.repository.hsqldb.HSQLDBRepository;
import org.qora.repository.hsqldb.HSQLDBSaver;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.TransactionType;
import static org.qora.transaction.Transaction.TransactionType.*;
@ -384,7 +387,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
LOGGER.trace(sql);
LOGGER.trace(String.format("Transaction search SQL: %s", sql));
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) {
if (resultSet == null)
@ -488,6 +491,59 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
@Override
public List<TransactionData> getPendingTransactions(Integer txGroupId, Integer limit, Integer offset, Boolean reverse) throws DataException {
String[] txTypesNeedingApproval = stream(Transaction.TransactionType.values())
.filter(txType -> txType.needsApproval)
.map(txType -> String.valueOf(txType.value))
.toArray(String[]::new);
String txTypes = String.join(", ", txTypesNeedingApproval);
/*
* We only want transactions matching certain types needing approval,
* with txGroupId not set to NO_GROUP and where auto-approval won't
* happen due to the transaction creator being an admin of that group.
*/
String sql = "SELECT signature FROM UnconfirmedTransactions "
+ "NATURAL JOIN Transactions "
+ "LEFT OUTER JOIN Accounts ON Accounts.public_key = Transactions.creator "
+ "LEFT OUTER JOIN GroupAdmins ON GroupAdmins.admin = Accounts.account "
+ "WHERE Transactions.tx_group_id != ? AND GroupAdmins.admin IS NULL "
+ "AND Transactions.type IN (" + txTypes + ") "
+ "ORDER BY creation";
if (reverse != null && reverse)
sql += " DESC";
sql += ", signature";
if (reverse != null && reverse)
sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
List<TransactionData> transactions = new ArrayList<TransactionData>();
// Find transactions with no corresponding row in BlockTransactions
try (ResultSet resultSet = this.repository.checkedExecute(sql, Group.NO_GROUP)) {
if (resultSet == null)
return transactions;
do {
byte[] signature = resultSet.getBytes(1);
TransactionData transactionData = this.fromSignature(signature);
if (transactionData == null)
// Something inconsistent with the repository
throw new DataException("Unable to fetch unconfirmed transaction from repository?");
transactions.add(transactionData);
} while (resultSet.next());
return transactions;
} catch (SQLException | DataException e) {
throw new DataException("Unable to fetch unconfirmed transactions from repository", e);
}
}
@Override
public int countTransactionApprovals(int txGroupId, byte[] signature) throws DataException {
// Fetch total number of approvals for signature

8
src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBUpdateGroupTransactionRepository.java

@ -19,7 +19,7 @@ public class HSQLDBUpdateGroupTransactionRepository extends HSQLDBTransactionRep
TransactionData fromBase(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, BigDecimal fee, byte[] signature) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT group_id, new_owner, new_description, new_is_open, new_approval_threshold, group_reference FROM UpdateGroupTransactions WHERE signature = ?",
"SELECT group_id, new_owner, new_description, new_is_open, new_approval_threshold, new_min_block_delay, new_max_block_delay, group_reference FROM UpdateGroupTransactions WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
@ -30,9 +30,11 @@ public class HSQLDBUpdateGroupTransactionRepository extends HSQLDBTransactionRep
boolean newIsOpen = resultSet.getBoolean(4);
ApprovalThreshold newApprovalThreshold = ApprovalThreshold.valueOf(resultSet.getInt(5));
byte[] groupReference = resultSet.getBytes(6);
int newMinBlockDelay = resultSet.getInt(7);
int newMaxBlockDelay = resultSet.getInt(8);
return new UpdateGroupTransactionData(timestamp, txGroupId, reference, creatorPublicKey, groupId, newOwner, newDescription, newIsOpen,
newApprovalThreshold, groupReference, fee, signature);
newApprovalThreshold, newMinBlockDelay, newMaxBlockDelay, groupReference, fee, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch update group transaction from repository", e);
}
@ -48,6 +50,8 @@ public class HSQLDBUpdateGroupTransactionRepository extends HSQLDBTransactionRep
.bind("group_id", updateGroupTransactionData.getGroupId()).bind("new_owner", updateGroupTransactionData.getNewOwner())
.bind("new_description", updateGroupTransactionData.getNewDescription()).bind("new_is_open", updateGroupTransactionData.getNewIsOpen())
.bind("new_approval_threshold", updateGroupTransactionData.getNewApprovalThreshold().value)
.bind("new_min_block_delay", updateGroupTransactionData.getNewMinimumBlockDelay())
.bind("new_max_block_delay", updateGroupTransactionData.getNewMaximumBlockDelay())
.bind("group_reference", updateGroupTransactionData.getGroupReference());
try {

12
src/main/java/org/qora/settings/Settings.java

@ -27,6 +27,7 @@ public class Settings {
private String userpath = "";
private boolean useBitcoinTestNet = false;
private boolean wipeUnconfirmedOnStart = false;
private Boolean restrictedApi;
private String blockchainConfigPath = "blockchain.json";
/** Maximum number of unconfirmed transactions allowed per account */
private int maxUnconfirmedPerAccount = 100;
@ -143,6 +144,9 @@ public class Settings {
if (json.containsKey("apiEnabled"))
this.apiEnabled = ((Boolean) json.get("apiEnabled")).booleanValue();
if (json.containsKey("restrictedApi"))
this.restrictedApi = ((Boolean) json.get("restrictedApi")).booleanValue();
// Peer-to-peer networking
if (json.containsKey("listenPort"))
@ -208,6 +212,14 @@ public class Settings {
return this.apiEnabled;
}
public boolean isRestrictedApi() {
if (this.restrictedApi != null)
return this.restrictedApi;
// Not set in config file, so restrict if not testnet
return !BlockChain.getInstance().getIsTestNet();
}
public int getListenPort() {
return this.listenPort;
}

9
src/main/java/org/qora/transaction/GroupApprovalTransaction.java

@ -69,15 +69,14 @@ public class GroupApprovalTransaction extends Transaction {
if (pendingTransactionData == null)
return ValidationResult.TRANSACTION_UNKNOWN;
// Check pending transaction's groupID matches our transaction's groupID
if (groupApprovalTransactionData.getTxGroupId() != pendingTransactionData.getTxGroupId())
return ValidationResult.GROUP_ID_MISMATCH;
// Check pending transaction is not already in a block
if (this.repository.getTransactionRepository().getHeightFromSignature(groupApprovalTransactionData.getPendingSignature()) != 0)
return ValidationResult.TRANSACTION_ALREADY_CONFIRMED;
// Check pending transaction's groupID matches our transaction's groupID
int effectiveTxGroupId = this.getEffectiveGroupId();
if (effectiveTxGroupId != pendingTransactionData.getTxGroupId())
return ValidationResult.GROUP_ID_MISMATCH;
Account admin = getAdmin();
// Can't cast approval decision if not an admin

22
src/main/java/org/qora/transaction/GroupInviteTransaction.java

@ -10,7 +10,6 @@ import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.data.transaction.GroupInviteTransactionData;
import org.qora.data.group.GroupData;
import org.qora.data.transaction.TransactionData;
import org.qora.group.Group;
import org.qora.repository.DataException;
@ -74,6 +73,12 @@ public class GroupInviteTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
int groupId = groupInviteTransactionData.getGroupId();
// Check transaction's groupID matches group's ID
if (groupInviteTransactionData.getTxGroupId() != groupId)
return ValidationResult.GROUP_ID_MISMATCH;
// Check time to live zero (infinite) or positive
if (groupInviteTransactionData.getTimeToLive() < 0)
return ValidationResult.INVALID_LIFETIME;
@ -82,31 +87,24 @@ public class GroupInviteTransaction extends Transaction {
if (!Crypto.isValidAddress(groupInviteTransactionData.getInvitee()))
return ValidationResult.INVALID_ADDRESS;
GroupData groupData = this.repository.getGroupRepository().fromGroupId(groupInviteTransactionData.getGroupId());
// Check group exists
if (groupData == null)
if (!this.repository.getGroupRepository().groupExists(groupId))
return ValidationResult.GROUP_DOES_NOT_EXIST;
// Check transaction's groupID matches group's ID
int effectiveTxGroupId = this.getEffectiveGroupId();
if (effectiveTxGroupId != groupInviteTransactionData.getTxGroupId())
return ValidationResult.GROUP_ID_MISMATCH;
Account admin = getAdmin();
// Can't invite if not an admin
if (!this.repository.getGroupRepository().adminExists(groupInviteTransactionData.getGroupId(), admin.getAddress()))
if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress()))
return ValidationResult.NOT_GROUP_ADMIN;
Account invitee = getInvitee();
// Check invitee not already in group
if (this.repository.getGroupRepository().memberExists(groupInviteTransactionData.getGroupId(), invitee.getAddress()))
if (this.repository.getGroupRepository().memberExists(groupId, invitee.getAddress()))
return ValidationResult.ALREADY_GROUP_MEMBER;
// Check invitee is not banned
if (this.repository.getGroupRepository().banExists(groupInviteTransactionData.getGroupId(), invitee.getAddress()))
if (this.repository.getGroupRepository().banExists(groupId, invitee.getAddress()))
return ValidationResult.BANNED_FROM_GROUP;
// Check fee is positive

16
src/main/java/org/qora/transaction/JoinGroupTransaction.java

@ -9,7 +9,6 @@ import org.qora.account.Account;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.data.transaction.JoinGroupTransactionData;
import org.qora.data.group.GroupData;
import org.qora.data.transaction.TransactionData;
import org.qora.group.Group;
import org.qora.repository.DataException;
@ -66,23 +65,28 @@ public class JoinGroupTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
GroupData groupData = this.repository.getGroupRepository().fromGroupId(joinGroupTransactionData.getGroupId());
int groupId = joinGroupTransactionData.getGroupId();
// Check txGroupId
int txGroupId = joinGroupTransactionData.getTxGroupId();
if (txGroupId != Group.NO_GROUP && txGroupId != groupId)
return ValidationResult.GROUP_ID_MISMATCH;
// Check group exists
if (groupData == null)
if (!this.repository.getGroupRepository().groupExists(groupId))
return ValidationResult.GROUP_DOES_NOT_EXIST;
Account joiner = getJoiner();
if (this.repository.getGroupRepository().memberExists(joinGroupTransactionData.getGroupId(), joiner.getAddress()))
if (this.repository.getGroupRepository().memberExists(groupId, joiner.getAddress()))
return ValidationResult.ALREADY_GROUP_MEMBER;
// Check member is not banned
if (this.repository.getGroupRepository().banExists(joinGroupTransactionData.getGroupId(), joiner.getAddress()))
if (this.repository.getGroupRepository().banExists(groupId, joiner.getAddress()))
return ValidationResult.BANNED_FROM_GROUP;
// Check join request doesn't already exist
if (this.repository.getGroupRepository().joinRequestExists(joinGroupTransactionData.getGroupId(), joiner.getAddress()))
if (this.repository.getGroupRepository().joinRequestExists(groupId, joiner.getAddress()))
return ValidationResult.JOIN_REQUEST_EXISTS;
// Check fee is positive

9
src/main/java/org/qora/transaction/SetGroupTransaction.java

@ -8,7 +8,6 @@ import java.util.List;
import org.qora.account.Account;
import org.qora.asset.Asset;
import org.qora.data.transaction.SetGroupTransactionData;
import org.qora.data.group.GroupData;
import org.qora.data.transaction.TransactionData;
import org.qora.group.Group;
import org.qora.repository.DataException;
@ -61,10 +60,8 @@ public class SetGroupTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
GroupData groupData = this.repository.getGroupRepository().fromGroupId(setGroupTransactionData.getDefaultGroupId());
// Check group exists
if (groupData == null)
if (!this.repository.getGroupRepository().groupExists(setGroupTransactionData.getDefaultGroupId()))
return ValidationResult.GROUP_DOES_NOT_EXIST;
Account creator = getCreator();
@ -94,7 +91,7 @@ public class SetGroupTransaction extends Transaction {
Integer previousDefaultGroupId = this.repository.getAccountRepository().getDefaultGroupId(creator.getAddress());
if (previousDefaultGroupId == null)
previousDefaultGroupId = Group.DEFAULT_GROUP;
previousDefaultGroupId = Group.NO_GROUP;
setGroupTransactionData.setPreviousDefaultGroupId(previousDefaultGroupId);
@ -118,7 +115,7 @@ public class SetGroupTransaction extends Transaction {
Integer previousDefaultGroupId = setGroupTransactionData.getPreviousDefaultGroupId();
if (previousDefaultGroupId == null)
previousDefaultGroupId = Group.DEFAULT_GROUP;
previousDefaultGroupId = Group.NO_GROUP;
creator.setDefaultGroupId(previousDefaultGroupId);

150
src/main/java/org/qora/transaction/Transaction.java

@ -16,6 +16,7 @@ import org.qora.account.PrivateKeyAccount;
import org.qora.account.PublicKeyAccount;
import org.qora.block.BlockChain;
import org.qora.data.block.BlockData;
import org.qora.data.group.GroupData;
import org.qora.data.transaction.TransactionData;
import org.qora.group.Group;
import org.qora.repository.DataException;
@ -37,43 +38,43 @@ public abstract class Transaction {
// Transaction types
public enum TransactionType {
// NOTE: must be contiguous or reflection fails
GENESIS(1, true),
GENESIS(1, false),
PAYMENT(2, false),
REGISTER_NAME(3, false),
UPDATE_NAME(4, false),
REGISTER_NAME(3, true),
UPDATE_NAME(4, true),
SELL_NAME(5, false),
CANCEL_SELL_NAME(6, false),
BUY_NAME(7, false),
CREATE_POLL(8, false),
CREATE_POLL(8, true),
VOTE_ON_POLL(9, false),
ARBITRARY(10, false),
ISSUE_ASSET(11, false),
ARBITRARY(10, true),
ISSUE_ASSET(11, true),
TRANSFER_ASSET(12, false),
CREATE_ASSET_ORDER(13, false),
CANCEL_ASSET_ORDER(14, false),
MULTI_PAYMENT(15, false),
DEPLOY_AT(16, false),
MESSAGE(17, false),
DEPLOY_AT(16, true),
MESSAGE(17, true),
DELEGATION(18, false),
SUPERNODE(19, false),
AIRDROP(20, true),
AT(21, true),
CREATE_GROUP(22, false),
AIRDROP(20, false),
AT(21, false),
CREATE_GROUP(22, true),
UPDATE_GROUP(23, true),
ADD_GROUP_ADMIN(24, true),
REMOVE_GROUP_ADMIN(25, true),
GROUP_BAN(26, true),
CANCEL_GROUP_BAN(27, true),
GROUP_KICK(28, true),
GROUP_INVITE(29, true),
CANCEL_GROUP_INVITE(30, true),
JOIN_GROUP(31, true),
LEAVE_GROUP(32, true),
GROUP_APPROVAL(33, true),
SET_GROUP(34, true);
ADD_GROUP_ADMIN(24, false),
REMOVE_GROUP_ADMIN(25, false),
GROUP_BAN(26, false),
CANCEL_GROUP_BAN(27, false),
GROUP_KICK(28, false),
GROUP_INVITE(29, false),
CANCEL_GROUP_INVITE(30, false),
JOIN_GROUP(31, false),
LEAVE_GROUP(32, false),
GROUP_APPROVAL(33, false),
SET_GROUP(34, false);
public final int value;
public final boolean skipsApproval;
public final boolean needsApproval;
public final String valueString;
public final String className;
public final Class<?> clazz;
@ -81,9 +82,9 @@ public abstract class Transaction {
private final static Map<Integer, TransactionType> map = stream(TransactionType.values()).collect(toMap(type -> type.value, type -> type));
TransactionType(int value, boolean skipsApproval) {
TransactionType(int value, boolean needsApproval) {
this.value = value;
this.skipsApproval = skipsApproval;
this.needsApproval = needsApproval;
this.valueString = String.valueOf(value);
String[] classNameParts = this.name().toLowerCase().split("_");
@ -495,19 +496,20 @@ public abstract class Transaction {
}
private boolean isValidTxGroupId() throws DataException {
// Does this transaction type bypass approval?
if (this.transactionData.getType().skipsApproval)
return true;
int txGroupId = this.transactionData.getTxGroupId();
int txGroupId = this.getEffectiveGroupId();
// Handling NO_GROUP
if (txGroupId == Group.NO_GROUP)
return true;
// true if NO_GROUP allowed, false otherwise
return BlockChain.getInstance().getGrouplessAllowed();
Group group = new Group(repository, txGroupId);
if (group.getGroupData() == null) {
// Group no longer exists? Possibly due to blockchain orphaning undoing group creation?
// Group even exist?
if (!this.repository.getGroupRepository().groupExists(txGroupId))
return false;
}
// Does this transaction type bypass approval?
if (!this.transactionData.getType().needsApproval)
return true;
GroupRepository groupRepository = this.repository.getGroupRepository();
@ -615,6 +617,17 @@ public abstract class Transaction {
if (transaction.getDeadline() <= blockTimestamp || transaction.getDeadline() < NTP.getTime())
return false;
// Is transaction is past max approval period?
if (transaction.needsGroupApproval()) {
int txGroupId = transactionData.getTxGroupId();
GroupData groupData = repository.getGroupRepository().fromGroupId(txGroupId);
int creationBlockHeight = repository.getBlockRepository().getHeightFromTimestamp(transactionData.getTimestamp());
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
if (currentBlockHeight > creationBlockHeight + groupData.getMaximumBlockDelay())
return false;
}
// Check transaction is currently valid
if (transaction.isValid() != Transaction.ValidationResult.OK)
return false;
@ -628,72 +641,47 @@ public abstract class Transaction {
}
/**
* Returns transaction's effective groupID, using default values where necessary.
*/
public int getEffectiveGroupId() throws DataException {
int txGroupId = this.transactionData.getTxGroupId();
// If transaction's groupID is NO_GROUP then group-ness doesn't apply
if (txGroupId == Group.NO_GROUP) {
if (BlockChain.getInstance().getGrouplessAllowed())
return txGroupId;
else
txGroupId = Group.DEFAULT_GROUP;
}
// If transaction's groupID is not DEFAULT_GROUP then no further processing required
if (txGroupId != Group.DEFAULT_GROUP)
return txGroupId;
// Try using account's default groupID
PublicKeyAccount creator = this.getCreator();
txGroupId = creator.getDefaultGroupId();
// If transaction's groupID is NO_GROUP then group-ness doesn't apply
if (txGroupId == Group.NO_GROUP) {
if (BlockChain.getInstance().getGrouplessAllowed())
return txGroupId;
else
txGroupId = Group.DEFAULT_GROUP;
}
// If txGroupId now not DEFAULT_GROUP then no further processing required
if (txGroupId != Group.DEFAULT_GROUP)
return txGroupId;
// Still zero? Use blockchain default
return BlockChain.getInstance().getDefaultGroupId();
}
/**
* Returns whether transaction still requires group-admin approval.
* Returns whether transaction needs to go through group-admin approval.
*
* @throws DataException
*/
public boolean needsGroupApproval() throws DataException {
// Does this transaction type bypass approval?
if (this.transactionData.getType().skipsApproval)
if (!this.transactionData.getType().needsApproval)
return false;
int txGroupId = this.getEffectiveGroupId();
int txGroupId = this.transactionData.getTxGroupId();
if (txGroupId == Group.NO_GROUP)
return false;
Group group = new Group(repository, txGroupId);
if (group.getGroupData() == null) {
GroupRepository groupRepository = this.repository.getGroupRepository();
if (!groupRepository.groupExists(txGroupId))
// Group no longer exists? Possibly due to blockchain orphaning undoing group creation?
return true;
}
GroupRepository groupRepository = this.repository.getGroupRepository();
// If transaction's creator is group admin then auto-approve
PublicKeyAccount creator = this.getCreator();
if (groupRepository.adminExists(txGroupId, creator.getAddress()))
return false;
return group.getGroupData().getApprovalThreshold().needsApproval(repository, txGroupId, this.transactionData.getSignature());
return true;
}
public boolean meetsGroupApprovalThreshold() throws DataException {
int txGroupId = this.transactionData.getTxGroupId();
Group group = new Group(repository, txGroupId);
GroupData groupData = group.getGroupData();
// Is transaction is outside of min/max approval period?
int creationBlockHeight = this.repository.getBlockRepository().getHeightFromTimestamp(this.transactionData.getTimestamp());
int currentBlockHeight = this.repository.getBlockRepository().getBlockchainHeight();
if (currentBlockHeight < creationBlockHeight + groupData.getMinimumBlockDelay() || currentBlockHeight > creationBlockHeight + groupData.getMaximumBlockDelay())
return false;
return group.getGroupData().getApprovalThreshold().meetsApprovalThreshold(repository, txGroupId, this.transactionData.getSignature());
}
/**

15
src/main/java/org/qora/transform/transaction/CreateGroupTransactionTransformer.java

@ -15,6 +15,7 @@ import org.qora.transform.TransformationException;
import org.qora.utils.Serialization;
import com.google.common.base.Utf8;
import com.google.common.primitives.Ints;
public class CreateGroupTransactionTransformer extends TransactionTransformer {
@ -42,6 +43,8 @@ public class CreateGroupTransactionTransformer extends TransactionTransformer {
layout.add("group's description", TransformationType.STRING);
layout.add("is group \"open\"?", TransformationType.BOOLEAN);
layout.add("group transaction approval threshold", TransformationType.BYTE);
layout.add("minimum block delay for transaction approvals", TransformationType.INT);
layout.add("maximum block delay for transaction approvals", TransformationType.INT);
layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE);
}
@ -68,13 +71,17 @@ public class CreateGroupTransactionTransformer extends TransactionTransformer {
ApprovalThreshold approvalThreshold = ApprovalThreshold.valueOf(byteBuffer.get());
int minBlockDelay = byteBuffer.getInt();
int maxBlockDelay = byteBuffer.getInt();
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
return new CreateGroupTransactionData(timestamp, txGroupId, reference, creatorPublicKey, owner, groupName, description, isOpen, approvalThreshold, null,
fee, signature);
return new CreateGroupTransactionData(timestamp, txGroupId, reference, creatorPublicKey, owner, groupName, description, isOpen, approvalThreshold,
minBlockDelay, maxBlockDelay, null, fee, signature);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
@ -102,6 +109,10 @@ public class CreateGroupTransactionTransformer extends TransactionTransformer {
bytes.write((byte) createGroupTransactionData.getApprovalThreshold().value);
bytes.write(Ints.toByteArray(createGroupTransactionData.getMinimumBlockDelay()));
bytes.write(Ints.toByteArray(createGroupTransactionData.getMaximumBlockDelay()));
Serialization.serializeBigDecimal(bytes, createGroupTransactionData.getFee());
if (createGroupTransactionData.getSignature() != null)

10
src/main/java/org/qora/transform/transaction/UpdateGroupTransactionTransformer.java

@ -70,13 +70,17 @@ public class UpdateGroupTransactionTransformer extends TransactionTransformer {
ApprovalThreshold newApprovalThreshold = ApprovalThreshold.valueOf(byteBuffer.get());
int newMinBlockDelay = byteBuffer.getInt();
int newMaxBlockDelay = byteBuffer.getInt();
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
return new UpdateGroupTransactionData(timestamp, txGroupId, reference, ownerPublicKey, groupId, newOwner, newDescription, newIsOpen,
newApprovalThreshold, fee, signature);
newApprovalThreshold, newMinBlockDelay, newMaxBlockDelay, fee, signature);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
@ -103,6 +107,10 @@ public class UpdateGroupTransactionTransformer extends TransactionTransformer {
bytes.write((byte) updateGroupTransactionData.getNewApprovalThreshold().value);
bytes.write(Ints.toByteArray(updateGroupTransactionData.getNewMinimumBlockDelay()));
bytes.write(Ints.toByteArray(updateGroupTransactionData.getNewMaximumBlockDelay()));
Serialization.serializeBigDecimal(bytes, updateGroupTransactionData.getFee());
if (updateGroupTransactionData.getSignature() != null)

4
src/main/java/org/qora/v1feeder.java

@ -496,13 +496,13 @@ public class v1feeder extends Thread {
BigDecimal fee = BigDecimal.ZERO.setScale(8);
TransactionData transactionData = new ATTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender, recipient, amount, Asset.QORA, message, fee);
TransactionData transactionData = new ATTransactionData(timestamp, Group.NO_GROUP, reference, sender, recipient, amount, Asset.QORA, message, fee);
byte[] digest;
try {
digest = Crypto.digest(AtTransactionTransformer.toBytes(transactionData));
byte[] signature = Bytes.concat(digest, digest);
transactionData = new ATTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender, recipient, amount, Asset.QORA, message, fee, signature);
transactionData = new ATTransactionData(timestamp, Group.NO_GROUP, reference, sender, recipient, amount, Asset.QORA, message, fee, signature);
} catch (TransformationException e) {
throw new RuntimeException("Couldn't transform AT Transaction into bytes", e);
}

1
src/main/resources/i18n/ApiError_en.properties

@ -7,6 +7,7 @@ NO_BALANCE=not enough balance
NOT_YET_RELEASED=that feature is not yet released
UNAUTHORIZED=api call unauthorized
REPOSITORY_ISSUE=repository error
NON_PRODUCTION=This API call is not permitted for production systems
# Validation
INVALID_SIGNATURE=invalid signature

2
src/test/java/org/qora/test/ATTests.java

@ -47,7 +47,7 @@ public class ATTests extends Common {
byte[] reference = Base58.decode("2D3jX1pEgu6irsQ7QzJb85QP1D9M45dNyP5M9a3WFHndU5ZywF4F5pnUurcbzMnGMcTwpAY6H7DuLw8cUBU66ao1");
byte[] signature = Base58.decode("2dZ4megUyNoYYY7qWmuSd4xw1yUKgPPF97yBbeddh8aKuC8PLpz7Xvf3r6Zjv1zwGrR8fEAHuaztCPD4KQp76KdL");
DeployAtTransactionData transactionData = new DeployAtTransactionData(timestamp, Group.DEFAULT_GROUP, reference, creatorPublicKey, name, description, ATType,
DeployAtTransactionData transactionData = new DeployAtTransactionData(timestamp, Group.NO_GROUP, reference, creatorPublicKey, name, description, ATType,
tags, creationBytes, amount, Asset.QORA, fee, signature);
try (final Repository repository = RepositoryManager.getRepository()) {

2
src/test/java/org/qora/test/SaveTests.java

@ -23,7 +23,7 @@ public class SaveTests extends Common {
byte[] signature = Base58.decode(signature58);
PublicKeyAccount sender = new PublicKeyAccount(repository, "Qsender".getBytes());
PaymentTransactionData paymentTransactionData = new PaymentTransactionData(Instant.now().getEpochSecond(), Group.DEFAULT_GROUP, reference,
PaymentTransactionData paymentTransactionData = new PaymentTransactionData(Instant.now().getEpochSecond(), Group.NO_GROUP, reference,
sender.getPublicKey(), "Qrecipient", BigDecimal.valueOf(12345L), BigDecimal.ONE, signature);
repository.getTransactionRepository().save(paymentTransactionData);

36
src/test/java/org/qora/test/TransactionTests.java

@ -121,7 +121,7 @@ public class TransactionTests {
// Create test generator account
generator = new PrivateKeyAccount(repository, generatorSeed);
accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.DEFAULT_GROUP));
accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.NO_GROUP));
accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORA, initialGeneratorBalance));
// Create test sender account
@ -129,7 +129,7 @@ public class TransactionTests {
// Mock account
reference = senderSeed;
accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.DEFAULT_GROUP));
accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.NO_GROUP));
// Mock balance
accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, initialSenderBalance));
@ -147,7 +147,7 @@ public class TransactionTests {
BigDecimal amount = genericPaymentAmount;
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
PaymentTransactionData paymentTransactionData = new PaymentTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(), recipient, amount, fee);
PaymentTransactionData paymentTransactionData = new PaymentTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), recipient, amount, fee);
Transaction paymentTransaction = new PaymentTransaction(repository, paymentTransactionData);
paymentTransaction.sign(sender);
@ -164,7 +164,7 @@ public class TransactionTests {
BigDecimal amount = BigDecimal.valueOf(1_000L);
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
PaymentTransactionData paymentTransactionData = new PaymentTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(), recipient.getAddress(),
PaymentTransactionData paymentTransactionData = new PaymentTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), recipient.getAddress(),
amount, fee);
Transaction paymentTransaction = new PaymentTransaction(repository, paymentTransactionData);
@ -225,7 +225,7 @@ public class TransactionTests {
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(), sender.getAddress(),
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), sender.getAddress(),
name, data, fee);
Transaction registerNameTransaction = new RegisterNameTransaction(repository, registerNameTransactionData);
@ -281,7 +281,7 @@ public class TransactionTests {
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
UpdateNameTransactionData updateNameTransactionData = new UpdateNameTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(),
UpdateNameTransactionData updateNameTransactionData = new UpdateNameTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(),
newOwner.getAddress(), name, newData, nameReference, fee);
Transaction updateNameTransaction = new UpdateNameTransaction(repository, updateNameTransactionData);
@ -327,7 +327,7 @@ public class TransactionTests {
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
SellNameTransactionData sellNameTransactionData = new SellNameTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(), name, amount, fee);
SellNameTransactionData sellNameTransactionData = new SellNameTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), name, amount, fee);
Transaction sellNameTransaction = new SellNameTransaction(repository, sellNameTransactionData);
sellNameTransaction.sign(sender);
@ -378,7 +378,7 @@ public class TransactionTests {
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
CancelSellNameTransactionData cancelSellNameTransactionData = new CancelSellNameTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(), name, fee);
CancelSellNameTransactionData cancelSellNameTransactionData = new CancelSellNameTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), name, fee);
Transaction cancelSellNameTransaction = new CancelSellNameTransaction(repository, cancelSellNameTransactionData);
cancelSellNameTransaction.sign(sender);
@ -443,7 +443,7 @@ public class TransactionTests {
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
BuyNameTransactionData buyNameTransactionData = new BuyNameTransactionData(timestamp, Group.DEFAULT_GROUP, buyersReference, buyer.getPublicKey(),
BuyNameTransactionData buyNameTransactionData = new BuyNameTransactionData(timestamp, Group.NO_GROUP, buyersReference, buyer.getPublicKey(),
name, originalNameData.getSalePrice(), seller, nameReference, fee);
Transaction buyNameTransaction = new BuyNameTransaction(repository, buyNameTransactionData);
@ -496,7 +496,7 @@ public class TransactionTests {
Account recipient = new PublicKeyAccount(repository, recipientSeed);
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
CreatePollTransactionData createPollTransactionData = new CreatePollTransactionData(timestamp, Group.DEFAULT_GROUP, reference,
CreatePollTransactionData createPollTransactionData = new CreatePollTransactionData(timestamp, Group.NO_GROUP, reference,
sender.getPublicKey(), recipient.getAddress(), pollName, description, pollOptions, fee);
Transaction createPollTransaction = new CreatePollTransaction(repository, createPollTransactionData);
@ -550,7 +550,7 @@ public class TransactionTests {
for (int optionIndex = 0; optionIndex <= pollOptionsSize; ++optionIndex) {
// Make a vote-on-poll transaction
VoteOnPollTransactionData voteOnPollTransactionData = new VoteOnPollTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(), pollName,
VoteOnPollTransactionData voteOnPollTransactionData = new VoteOnPollTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), pollName,
optionIndex, fee);
Transaction voteOnPollTransaction = new VoteOnPollTransaction(repository, voteOnPollTransactionData);
@ -622,7 +622,7 @@ public class TransactionTests {
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
IssueAssetTransactionData issueAssetTransactionData = new IssueAssetTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(),
IssueAssetTransactionData issueAssetTransactionData = new IssueAssetTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(),
sender.getAddress(), assetName, description, quantity, isDivisible, fee);
Transaction issueAssetTransaction = new IssueAssetTransaction(repository, issueAssetTransactionData);
@ -712,7 +712,7 @@ public class TransactionTests {
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
TransferAssetTransactionData transferAssetTransactionData = new TransferAssetTransactionData(timestamp, Group.DEFAULT_GROUP, reference,
TransferAssetTransactionData transferAssetTransactionData = new TransferAssetTransactionData(timestamp, Group.NO_GROUP, reference,
sender.getPublicKey(), recipient.getAddress(), amount, assetId, fee);
Transaction transferAssetTransaction = new TransferAssetTransaction(repository, transferAssetTransactionData);
@ -817,7 +817,7 @@ public class TransactionTests {
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
CreateAssetOrderTransactionData createOrderTransactionData = new CreateAssetOrderTransactionData(timestamp, Group.DEFAULT_GROUP, buyersReference, buyer.getPublicKey(), haveAssetId,
CreateAssetOrderTransactionData createOrderTransactionData = new CreateAssetOrderTransactionData(timestamp, Group.NO_GROUP, buyersReference, buyer.getPublicKey(), haveAssetId,
wantAssetId, amount, price, fee);
Transaction createOrderTransaction = new CreateAssetOrderTransaction(this.repository, createOrderTransactionData);
createOrderTransaction.sign(buyer);
@ -898,7 +898,7 @@ public class TransactionTests {
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
byte[] buyersReference = buyer.getLastReference();
CancelAssetOrderTransactionData cancelOrderTransactionData = new CancelAssetOrderTransactionData(timestamp, Group.DEFAULT_GROUP, buyersReference, buyer.getPublicKey(), orderId, fee);
CancelAssetOrderTransactionData cancelOrderTransactionData = new CancelAssetOrderTransactionData(timestamp, Group.NO_GROUP, buyersReference, buyer.getPublicKey(), orderId, fee);
Transaction cancelOrderTransaction = new CancelAssetOrderTransaction(this.repository, cancelOrderTransactionData);
cancelOrderTransaction.sign(buyer);
@ -973,7 +973,7 @@ public class TransactionTests {
long timestamp = parentBlockData.getTimestamp() + 1_000;
BigDecimal senderPreTradeWantBalance = sender.getConfirmedBalance(wantAssetId);
CreateAssetOrderTransactionData createOrderTransactionData = new CreateAssetOrderTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(), haveAssetId,
CreateAssetOrderTransactionData createOrderTransactionData = new CreateAssetOrderTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), haveAssetId,
wantAssetId, amount, price, fee);
Transaction createOrderTransaction = new CreateAssetOrderTransaction(this.repository, createOrderTransactionData);
createOrderTransaction.sign(sender);
@ -1082,7 +1082,7 @@ public class TransactionTests {
payments.add(paymentData);
}
MultiPaymentTransactionData multiPaymentTransactionData = new MultiPaymentTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(), payments, fee);
MultiPaymentTransactionData multiPaymentTransactionData = new MultiPaymentTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), payments, fee);
Transaction multiPaymentTransaction = new MultiPaymentTransaction(repository, multiPaymentTransactionData);
multiPaymentTransaction.sign(sender);
@ -1151,7 +1151,7 @@ public class TransactionTests {
boolean isText = true;
boolean isEncrypted = false;
MessageTransactionData messageTransactionData = new MessageTransactionData(timestamp, Group.DEFAULT_GROUP, reference, sender.getPublicKey(), version,
MessageTransactionData messageTransactionData = new MessageTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), version,
recipient.getAddress(), Asset.QORA, amount, data, isText, isEncrypted, fee);
Transaction messageTransaction = new MessageTransaction(repository, messageTransactionData);

Loading…
Cancel
Save