forked from Qortal/qortal
Merge branch 'null-owned-groups'
This commit is contained in:
commit
a7402adfa5
@ -128,6 +128,10 @@ public abstract class TransactionData {
|
|||||||
return this.txGroupId;
|
return this.txGroupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTxGroupId(int txGroupId) {
|
||||||
|
this.txGroupId = txGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] getReference() {
|
public byte[] getReference() {
|
||||||
return this.reference;
|
return this.reference;
|
||||||
}
|
}
|
||||||
|
@ -80,6 +80,9 @@ public class Group {
|
|||||||
// Useful constants
|
// Useful constants
|
||||||
public static final int NO_GROUP = 0;
|
public static final int NO_GROUP = 0;
|
||||||
|
|
||||||
|
// Null owner address corresponds with public key "11111111111111111111111111111111"
|
||||||
|
public static String NULL_OWNER_ADDRESS = "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG";
|
||||||
|
|
||||||
public static final int MIN_NAME_SIZE = 3;
|
public static final int MIN_NAME_SIZE = 3;
|
||||||
public static final int MAX_NAME_SIZE = 32;
|
public static final int MAX_NAME_SIZE = 32;
|
||||||
public static final int MAX_DESCRIPTION_SIZE = 128;
|
public static final int MAX_DESCRIPTION_SIZE = 128;
|
||||||
|
@ -2,6 +2,7 @@ package org.qortal.transaction;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
@ -64,15 +65,24 @@ public class AddGroupAdminTransaction extends Transaction {
|
|||||||
|
|
||||||
Account owner = getOwner();
|
Account owner = getOwner();
|
||||||
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||||
|
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||||
|
|
||||||
// Check transaction's public key matches group's current owner
|
// Require approval if transaction relates to a group owned by the null account
|
||||||
if (!owner.getAddress().equals(groupOwner))
|
if (groupOwnedByNullAccount && !this.needsGroupApproval())
|
||||||
|
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||||
|
|
||||||
|
// Check transaction's public key matches group's current owner (except for groups owned by the null account)
|
||||||
|
if (!groupOwnedByNullAccount && !owner.getAddress().equals(groupOwner))
|
||||||
return ValidationResult.INVALID_GROUP_OWNER;
|
return ValidationResult.INVALID_GROUP_OWNER;
|
||||||
|
|
||||||
// Check address is a group member
|
// Check address is a group member
|
||||||
if (!this.repository.getGroupRepository().memberExists(groupId, memberAddress))
|
if (!this.repository.getGroupRepository().memberExists(groupId, memberAddress))
|
||||||
return ValidationResult.NOT_GROUP_MEMBER;
|
return ValidationResult.NOT_GROUP_MEMBER;
|
||||||
|
|
||||||
|
// Check transaction creator is a group member
|
||||||
|
if (!this.repository.getGroupRepository().memberExists(groupId, this.getCreator().getAddress()))
|
||||||
|
return ValidationResult.NOT_GROUP_MEMBER;
|
||||||
|
|
||||||
// Check group member is not already an admin
|
// Check group member is not already an admin
|
||||||
if (this.repository.getGroupRepository().adminExists(groupId, memberAddress))
|
if (this.repository.getGroupRepository().adminExists(groupId, memberAddress))
|
||||||
return ValidationResult.ALREADY_GROUP_ADMIN;
|
return ValidationResult.ALREADY_GROUP_ADMIN;
|
||||||
|
@ -2,6 +2,7 @@ package org.qortal.transaction;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
@ -65,11 +66,21 @@ public class RemoveGroupAdminTransaction extends Transaction {
|
|||||||
return ValidationResult.GROUP_DOES_NOT_EXIST;
|
return ValidationResult.GROUP_DOES_NOT_EXIST;
|
||||||
|
|
||||||
Account owner = getOwner();
|
Account owner = getOwner();
|
||||||
|
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||||
|
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||||
|
|
||||||
// Check transaction's public key matches group's current owner
|
// Require approval if transaction relates to a group owned by the null account
|
||||||
if (!owner.getAddress().equals(groupData.getOwner()))
|
if (groupOwnedByNullAccount && !this.needsGroupApproval())
|
||||||
|
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||||
|
|
||||||
|
// Check transaction's public key matches group's current owner (except for groups owned by the null account)
|
||||||
|
if (!groupOwnedByNullAccount && !owner.getAddress().equals(groupOwner))
|
||||||
return ValidationResult.INVALID_GROUP_OWNER;
|
return ValidationResult.INVALID_GROUP_OWNER;
|
||||||
|
|
||||||
|
// Check transaction creator is a group member
|
||||||
|
if (!this.repository.getGroupRepository().memberExists(groupId, this.getCreator().getAddress()))
|
||||||
|
return ValidationResult.NOT_GROUP_MEMBER;
|
||||||
|
|
||||||
Account admin = getAdmin();
|
Account admin = getAdmin();
|
||||||
|
|
||||||
// Check member is an admin
|
// Check member is an admin
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
package org.qortal.transaction;
|
package org.qortal.transaction;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
@ -69,8 +63,8 @@ public abstract class Transaction {
|
|||||||
AT(21, false),
|
AT(21, false),
|
||||||
CREATE_GROUP(22, true),
|
CREATE_GROUP(22, true),
|
||||||
UPDATE_GROUP(23, true),
|
UPDATE_GROUP(23, true),
|
||||||
ADD_GROUP_ADMIN(24, false),
|
ADD_GROUP_ADMIN(24, true),
|
||||||
REMOVE_GROUP_ADMIN(25, false),
|
REMOVE_GROUP_ADMIN(25, true),
|
||||||
GROUP_BAN(26, false),
|
GROUP_BAN(26, false),
|
||||||
CANCEL_GROUP_BAN(27, false),
|
CANCEL_GROUP_BAN(27, false),
|
||||||
GROUP_KICK(28, false),
|
GROUP_KICK(28, false),
|
||||||
@ -250,6 +244,7 @@ public abstract class Transaction {
|
|||||||
INVALID_TIMESTAMP_SIGNATURE(95),
|
INVALID_TIMESTAMP_SIGNATURE(95),
|
||||||
ADDRESS_BLOCKED(96),
|
ADDRESS_BLOCKED(96),
|
||||||
NAME_BLOCKED(97),
|
NAME_BLOCKED(97),
|
||||||
|
GROUP_APPROVAL_REQUIRED(98),
|
||||||
INVALID_BUT_OK(999),
|
INVALID_BUT_OK(999),
|
||||||
NOT_YET_RELEASED(1000);
|
NOT_YET_RELEASED(1000);
|
||||||
|
|
||||||
@ -760,9 +755,13 @@ public abstract class Transaction {
|
|||||||
// Group no longer exists? Possibly due to blockchain orphaning undoing group creation?
|
// Group no longer exists? Possibly due to blockchain orphaning undoing group creation?
|
||||||
return true; // stops tx being included in block but it will eventually expire
|
return true; // stops tx being included in block but it will eventually expire
|
||||||
|
|
||||||
|
String groupOwner = this.repository.getGroupRepository().getOwner(txGroupId);
|
||||||
|
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||||
|
|
||||||
// If transaction's creator is group admin (of group with ID txGroupId) then auto-approve
|
// If transaction's creator is group admin (of group with ID txGroupId) then auto-approve
|
||||||
|
// This is disabled for null-owned groups, since these require approval from other admins
|
||||||
PublicKeyAccount creator = this.getCreator();
|
PublicKeyAccount creator = this.getCreator();
|
||||||
if (groupRepository.adminExists(txGroupId, creator.getAddress()))
|
if (!groupOwnedByNullAccount && groupRepository.adminExists(txGroupId, creator.getAddress()))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
388
src/test/java/org/qortal/test/group/DevGroupAdminTests.java
Normal file
388
src/test/java/org/qortal/test/group/DevGroupAdminTests.java
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
package org.qortal.test.group;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.data.transaction.*;
|
||||||
|
import org.qortal.group.Group;
|
||||||
|
import org.qortal.group.Group.ApprovalThreshold;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.test.common.BlockUtils;
|
||||||
|
import org.qortal.test.common.Common;
|
||||||
|
import org.qortal.test.common.GroupUtils;
|
||||||
|
import org.qortal.test.common.TransactionUtils;
|
||||||
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.Transaction;
|
||||||
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dev group admin tests
|
||||||
|
*
|
||||||
|
* The dev group (ID 1) is owned by the null account with public key 11111111111111111111111111111111
|
||||||
|
* To regain access to otherwise blocked owner-based rules, it has different validation logic
|
||||||
|
* which applies to groups with this same null owner.
|
||||||
|
*
|
||||||
|
* The main difference is that approval is required for certain transaction types relating to
|
||||||
|
* null-owned groups. This allows existing admins to approve updates to the group (using group's
|
||||||
|
* approval threshold) instead of these actions being performed by the owner.
|
||||||
|
*
|
||||||
|
* Since these apply to all null-owned groups, this allows anyone to update their group to
|
||||||
|
* the null owner if they want to take advantage of this decentralized approval system.
|
||||||
|
*
|
||||||
|
* Currently, the affected transaction types are:
|
||||||
|
* - AddGroupAdminTransaction
|
||||||
|
* - RemoveGroupAdminTransaction
|
||||||
|
*
|
||||||
|
* This same approach could ultimately be applied to other group transactions too.
|
||||||
|
*/
|
||||||
|
public class DevGroupAdminTests extends Common {
|
||||||
|
|
||||||
|
private static final int DEV_GROUP_ID = 1;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeTest() throws DataException {
|
||||||
|
Common.useDefaultSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void afterTest() throws DataException {
|
||||||
|
Common.orphanCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupKickMember() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
|
|
||||||
|
// Dev group
|
||||||
|
int groupId = DEV_GROUP_ID;
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to kick Bob
|
||||||
|
ValidationResult result = groupKick(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Alice to invite Bob, as it's a closed group
|
||||||
|
groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
|
||||||
|
|
||||||
|
// Bob to join
|
||||||
|
joinGroup(repository, bob, groupId);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to kick Bob
|
||||||
|
result = groupKick(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob no longer a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Orphan last block
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupKickAdmin() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
|
|
||||||
|
// Dev group
|
||||||
|
int groupId = DEV_GROUP_ID;
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Alice to invite Bob, as it's a closed group
|
||||||
|
groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
|
||||||
|
|
||||||
|
// Bob to join
|
||||||
|
joinGroup(repository, bob, groupId);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Promote Bob to admin
|
||||||
|
TransactionData addGroupAdminTransactionData = addGroupAdmin(repository, alice, groupId, bob.getAddress());
|
||||||
|
|
||||||
|
// Confirm transaction needs approval, and hasn't been approved
|
||||||
|
Transaction.ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
|
||||||
|
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus);
|
||||||
|
|
||||||
|
// Have Alice approve Bob's approval-needed transaction
|
||||||
|
GroupUtils.approveTransaction(repository, "alice", addGroupAdminTransactionData.getSignature(), true);
|
||||||
|
|
||||||
|
// Mint a block so that the transaction becomes approved
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
// Confirm transaction is approved
|
||||||
|
approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
|
||||||
|
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.APPROVED, approvalStatus);
|
||||||
|
|
||||||
|
// Confirm Bob is now admin
|
||||||
|
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to kick Bob
|
||||||
|
ValidationResult result = groupKick(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Shouldn't be allowed
|
||||||
|
assertEquals(ValidationResult.INVALID_GROUP_OWNER, result);
|
||||||
|
|
||||||
|
// Confirm Bob is still a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Confirm Bob still an admin
|
||||||
|
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Orphan last block
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
|
// Confirm Bob no longer an admin (ADD_GROUP_ADMIN no longer approved)
|
||||||
|
assertFalse(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Have Alice try to kick herself!
|
||||||
|
result = groupKick(repository, alice, groupId, alice.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Have Bob try to kick Alice
|
||||||
|
result = groupKick(repository, bob, groupId, alice.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupBanMember() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
|
|
||||||
|
// Dev group
|
||||||
|
int groupId = DEV_GROUP_ID;
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to cancel non-existent Bob ban
|
||||||
|
ValidationResult result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Attempt to ban Bob
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Orphan last block (Bob ban)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed group-ban transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Alice to invite Bob, as it's a closed group
|
||||||
|
groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
|
||||||
|
|
||||||
|
// Bob to join
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to ban Bob
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob no longer a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Cancel Bob's ban
|
||||||
|
result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Orphan last block (Bob join)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed join-group transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Orphan last block (Cancel Bob ban)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed cancel-ban transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Orphan last block (Bob ban)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed group-ban transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupBanAdmin() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
|
|
||||||
|
// Dev group
|
||||||
|
int groupId = DEV_GROUP_ID;
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Alice to invite Bob, as it's a closed group
|
||||||
|
groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
|
||||||
|
|
||||||
|
// Bob to join
|
||||||
|
ValidationResult result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Promote Bob to admin
|
||||||
|
TransactionData addGroupAdminTransactionData = addGroupAdmin(repository, alice, groupId, bob.getAddress());
|
||||||
|
|
||||||
|
// Confirm transaction needs approval, and hasn't been approved
|
||||||
|
Transaction.ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
|
||||||
|
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus);
|
||||||
|
|
||||||
|
// Have Alice approve Bob's approval-needed transaction
|
||||||
|
GroupUtils.approveTransaction(repository, "alice", addGroupAdminTransactionData.getSignature(), true);
|
||||||
|
|
||||||
|
// Mint a block so that the transaction becomes approved
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
// Confirm transaction is approved
|
||||||
|
approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
|
||||||
|
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.APPROVED, approvalStatus);
|
||||||
|
|
||||||
|
// Confirm Bob is now admin
|
||||||
|
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to ban Bob
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// .. but we can't, because Bob is an admin and the group has no owner
|
||||||
|
assertEquals(ValidationResult.INVALID_GROUP_OWNER, result);
|
||||||
|
|
||||||
|
// Confirm Bob still a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// ... and still an admin
|
||||||
|
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Have Alice try to ban herself!
|
||||||
|
result = groupBan(repository, alice, groupId, alice.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Have Bob try to ban Alice
|
||||||
|
result = groupBan(repository, bob, groupId, alice.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException {
|
||||||
|
JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId);
|
||||||
|
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, joiner);
|
||||||
|
|
||||||
|
if (result == ValidationResult.OK)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
|
||||||
|
GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin), groupId, invitee, timeToLive);
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, admin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationResult groupKick(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
|
||||||
|
GroupKickTransactionData transactionData = new GroupKickTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing");
|
||||||
|
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||||
|
|
||||||
|
if (result == ValidationResult.OK)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
|
||||||
|
GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", 0);
|
||||||
|
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||||
|
|
||||||
|
if (result == ValidationResult.OK)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationResult cancelGroupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
|
||||||
|
CancelGroupBanTransactionData transactionData = new CancelGroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member);
|
||||||
|
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||||
|
|
||||||
|
if (result == ValidationResult.OK)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransactionData addGroupAdmin(Repository repository, PrivateKeyAccount owner, int groupId, String member) throws DataException {
|
||||||
|
AddGroupAdminTransactionData transactionData = new AddGroupAdminTransactionData(TestTransaction.generateBase(owner), groupId, member);
|
||||||
|
transactionData.setTxGroupId(groupId);
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, owner);
|
||||||
|
return transactionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMember(Repository repository, String address, int groupId) throws DataException {
|
||||||
|
return repository.getGroupRepository().memberExists(groupId, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAdmin(Repository repository, String address, int groupId) throws DataException {
|
||||||
|
return repository.getGroupRepository().adminExists(groupId, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -91,6 +91,8 @@
|
|||||||
|
|
||||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
||||||
|
|
||||||
|
{ "type": "UPDATE_GROUP", "ownerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupId": 1, "newOwner": "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT40", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 },
|
||||||
|
|
||||||
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
||||||
{ "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
{ "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
||||||
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
||||||
|
97
tools/approve-dev-transaction.sh
Executable file
97
tools/approve-dev-transaction.sh
Executable file
@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
port=12391
|
||||||
|
if [ $# -gt 0 -a "$1" = "-t" ]; then
|
||||||
|
port=62391
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "Searching for auto-update transactions to approve...\n";
|
||||||
|
|
||||||
|
tx=$( curl --silent --url "http://localhost:${port}/transactions/search?txGroupId=1&txType=ADD_GROUP_ADMIN&txType=REMOVE_GROUP_ADMIN&confirmationStatus=CONFIRMED&limit=1&reverse=true" );
|
||||||
|
if fgrep --silent '"approvalStatus":"PENDING"' <<< "${tx}"; then
|
||||||
|
true
|
||||||
|
else
|
||||||
|
echo "Can't find any pending transactions"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
sig=$( perl -n -e 'print $1 if m/"signature":"(\w+)"/' <<< "${tx}" )
|
||||||
|
if [ -z "${sig}" ]; then
|
||||||
|
printf "Can't find transaction signature in JSON:\n%s\n" "${tx}"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "Found transaction %s\n" $sig;
|
||||||
|
|
||||||
|
printf "\nPaste your dev account private key:\n";
|
||||||
|
IFS=
|
||||||
|
read -s privkey
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
# Convert to public key
|
||||||
|
pubkey=$( curl --silent --url "http://localhost:${port}/utils/publickey" --data @- <<< "${privkey}" );
|
||||||
|
if egrep -v --silent '^\w{44,46}$' <<< "${pubkey}"; then
|
||||||
|
printf "Invalid response from API - was your private key correct?\n%s\n" "${pubkey}"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
printf "Your public key: %s\n" ${pubkey}
|
||||||
|
|
||||||
|
# Convert to address
|
||||||
|
address=$( curl --silent --url "http://localhost:${port}/addresses/convert/${pubkey}" );
|
||||||
|
printf "Your address: %s\n" ${address}
|
||||||
|
|
||||||
|
# Grab last reference
|
||||||
|
lastref=$( curl --silent --url "http://localhost:${port}/addresses/lastreference/{$address}" );
|
||||||
|
printf "Your last reference: %s\n" ${lastref}
|
||||||
|
|
||||||
|
# Build GROUP_APPROVAL transaction
|
||||||
|
timestamp=$( date +%s )000
|
||||||
|
tx_json=$( cat <<TX_END
|
||||||
|
{
|
||||||
|
"timestamp": ${timestamp},
|
||||||
|
"reference": "${lastref}",
|
||||||
|
"fee": 0.001,
|
||||||
|
"txGroupId": 0,
|
||||||
|
"adminPublicKey": "${pubkey}",
|
||||||
|
"pendingSignature": "${sig}",
|
||||||
|
"approval": true
|
||||||
|
}
|
||||||
|
TX_END
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_tx=$( curl --silent --header "Content-Type: application/json" --url "http://localhost:${port}/groups/approval" --data @- <<< "${tx_json}" )
|
||||||
|
if egrep -v --silent '^\w{100,}' <<< "${raw_tx}"; then
|
||||||
|
printf "Building GROUP_APPROVAL transaction failed:\n%s\n" "${raw_tx}"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
printf "\nRaw approval tx:\n%s\n" ${raw_tx}
|
||||||
|
|
||||||
|
# sign
|
||||||
|
sign_json=$( cat <<SIGN_END
|
||||||
|
{
|
||||||
|
"privateKey": "${privkey}",
|
||||||
|
"transactionBytes": "${raw_tx}"
|
||||||
|
}
|
||||||
|
SIGN_END
|
||||||
|
)
|
||||||
|
signed_tx=$( curl --silent --header "Content-Type: application/json" --url "http://localhost:${port}/transactions/sign" --data @- <<< "${sign_json}" )
|
||||||
|
printf "\nSigned tx:\n%s\n" ${signed_tx}
|
||||||
|
if egrep -v --silent '^\w{100,}' <<< "${signed_tx}"; then
|
||||||
|
printf "Signing GROUP_APPROVAL transaction failed:\n%s\n" "${signed_tx}"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ready to publish?
|
||||||
|
plural="s"
|
||||||
|
printf "\n"
|
||||||
|
for ((seconds = 5; seconds > 0; seconds--)); do
|
||||||
|
if [ "${seconds}" = "1" ]; then
|
||||||
|
plural=""
|
||||||
|
fi
|
||||||
|
printf "\rBroadcasting in %d second%s...(CTRL-C) to abort " $seconds $plural
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
printf "\rBroadcasting signed GROUP_APPROVAL transaction... \n"
|
||||||
|
result=$( curl --silent --url "http://localhost:${port}/transactions/process" --data @- <<< "${signed_tx}" )
|
||||||
|
printf "API response:\n%s\n" "${result}"
|
@ -71,9 +71,14 @@ our %TRANSACTION_TYPES = (
|
|||||||
},
|
},
|
||||||
add_group_admin => {
|
add_group_admin => {
|
||||||
url => 'groups/addadmin',
|
url => 'groups/addadmin',
|
||||||
required => [qw(groupId member)],
|
required => [qw(groupId txGroupId member)],
|
||||||
key_name => 'ownerPublicKey',
|
key_name => 'ownerPublicKey',
|
||||||
},
|
},
|
||||||
|
remove_group_admin => {
|
||||||
|
url => 'groups/removeadmin',
|
||||||
|
required => [qw(groupId txGroupId admin)],
|
||||||
|
key_name => 'ownerPublicKey',
|
||||||
|
},
|
||||||
group_approval => {
|
group_approval => {
|
||||||
url => 'groups/approval',
|
url => 'groups/approval',
|
||||||
required => [qw(pendingSignature approval)],
|
required => [qw(pendingSignature approval)],
|
||||||
|
Loading…
Reference in New Issue
Block a user