forked from Qortal/qortal
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
This commit is contained in:
parent
00656f6724
commit
86a35c3b71
@ -1,6 +0,0 @@
|
||||
{
|
||||
"rpcallowed": [
|
||||
"::/0",
|
||||
"0.0.0.0/0"
|
||||
]
|
||||
}
|
@ -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),
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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")) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,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)
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()) {
|
||||
|
@ -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);
|
||||
|
@ -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…
Reference in New Issue
Block a user