package protocol import ( "bytes" "crypto/ecdsa" "errors" "math/big" "sync" "testing" "time" "github.com/stretchr/testify/suite" "go.uber.org/zap" gethcommon "github.com/ethereum/go-ethereum/common" hexutil "github.com/ethereum/go-ethereum/common/hexutil" gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/communities" "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/tt" "github.com/status-im/status-go/waku" ) const testChainID1 = 1 const ownerPassword = "123456" const alicePassword = "qwerty" const bobPassword = "bob123" const ownerAddress = "0x0100000000000000000000000000000000000000" const aliceAddress1 = "0x0200000000000000000000000000000000000000" const aliceAddress2 = "0x0210000000000000000000000000000000000000" const bobAddress = "0x0300000000000000000000000000000000000000" type CommunityAndKeyActions struct { community *communities.Community keyActions *communities.EncryptionKeyActions } type TestCommunitiesKeyDistributor struct { CommunitiesKeyDistributorImpl subscriptions map[chan *CommunityAndKeyActions]bool mutex sync.RWMutex } func (tckd *TestCommunitiesKeyDistributor) Distribute(community *communities.Community, keyActions *communities.EncryptionKeyActions) error { err := tckd.CommunitiesKeyDistributorImpl.Distribute(community, keyActions) if err != nil { return err } // notify distribute finished tckd.mutex.RLock() for s := range tckd.subscriptions { s <- &CommunityAndKeyActions{ community: community, keyActions: keyActions, } } tckd.mutex.RUnlock() return nil } func (tckd *TestCommunitiesKeyDistributor) waitOnKeyDistribution(condition func(*CommunityAndKeyActions) bool) <-chan error { errCh := make(chan error, 1) subscription := make(chan *CommunityAndKeyActions) tckd.mutex.Lock() tckd.subscriptions[subscription] = true tckd.mutex.Unlock() go func() { defer func() { close(errCh) tckd.mutex.Lock() delete(tckd.subscriptions, subscription) tckd.mutex.Unlock() close(subscription) }() for { select { case s, more := <-subscription: if !more { errCh <- errors.New("channel closed when waiting for key distribution") return } if condition(s) { return } case <-time.After(500 * time.Millisecond): errCh <- errors.New("timed out when waiting for key distribution") return } } }() return errCh } func TestMessengerCommunitiesTokenPermissionsSuite(t *testing.T) { suite.Run(t, new(MessengerCommunitiesTokenPermissionsSuite)) } type MessengerCommunitiesTokenPermissionsSuite struct { suite.Suite owner *Messenger bob *Messenger alice *Messenger // If one wants to send messages between different instances of Messenger, // a single Waku service should be shared. shh types.Waku logger *zap.Logger mockedBalances map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big // chainID, account, token, balance collectiblesServiceMock *CollectiblesServiceMock } func (s *MessengerCommunitiesTokenPermissionsSuite) SetupTest() { s.logger = tt.MustCreateTestLogger() config := waku.DefaultConfig config.MinimumAcceptedPoW = 0 shh := waku.New(&config, s.logger) s.shh = gethbridge.NewGethWakuWrapper(shh) s.Require().NoError(shh.Start()) s.owner = s.newMessenger(ownerPassword, []string{ownerAddress}) s.bob = s.newMessenger(bobPassword, []string{bobAddress}) s.alice = s.newMessenger(alicePassword, []string{aliceAddress1, aliceAddress2}) _, err := s.owner.Start() s.Require().NoError(err) _, err = s.bob.Start() s.Require().NoError(err) _, err = s.alice.Start() s.Require().NoError(err) s.mockedBalances = make(map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big) s.mockedBalances[testChainID1] = make(map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big) s.mockedBalances[testChainID1][gethcommon.HexToAddress(aliceAddress1)] = make(map[gethcommon.Address]*hexutil.Big) s.mockedBalances[testChainID1][gethcommon.HexToAddress(aliceAddress2)] = make(map[gethcommon.Address]*hexutil.Big) s.mockedBalances[testChainID1][gethcommon.HexToAddress(bobAddress)] = make(map[gethcommon.Address]*hexutil.Big) } func (s *MessengerCommunitiesTokenPermissionsSuite) TearDownTest() { s.Require().NoError(s.owner.Shutdown()) s.Require().NoError(s.bob.Shutdown()) s.Require().NoError(s.alice.Shutdown()) _ = s.logger.Sync() } func (s *MessengerCommunitiesTokenPermissionsSuite) newMessenger(password string, walletAddresses []string) *Messenger { return newMessenger(&s.Suite, s.shh, s.logger, password, walletAddresses, &s.mockedBalances, s.collectiblesServiceMock) } func (s *MessengerCommunitiesTokenPermissionsSuite) joinCommunity(community *communities.Community, user *Messenger, password string, addresses []string) { s.joinCommunityWithAirdropAddress(community, user, password, addresses, "") } func (s *MessengerCommunitiesTokenPermissionsSuite) joinCommunityWithAirdropAddress(community *communities.Community, user *Messenger, password string, addresses []string, airdropAddress string) { passwdHash := types.EncodeHex(crypto.Keccak256([]byte(password))) if airdropAddress == "" && len(addresses) > 0 { airdropAddress = addresses[0] } request := &requests.RequestToJoinCommunity{CommunityID: community.ID(), Password: passwdHash, AddressesToReveal: addresses, AirdropAddress: airdropAddress} joinCommunity(&s.Suite, community, s.owner, user, request) } func (s *MessengerCommunitiesTokenPermissionsSuite) advertiseCommunityTo(community *communities.Community, user *Messenger) { advertiseCommunityTo(&s.Suite, community, s.owner, user) } func (s *MessengerCommunitiesTokenPermissionsSuite) createCommunity() (*communities.Community, *Chat) { return createCommunity(&s.Suite, s.owner) } func (s *MessengerCommunitiesTokenPermissionsSuite) sendChatMessage(sender *Messenger, chatID string, text string) *common.Message { return sendChatMessage(&s.Suite, sender, chatID, text) } func (s *MessengerCommunitiesTokenPermissionsSuite) makeAddressSatisfyTheCriteria(chainID uint64, address string, criteria *protobuf.TokenCriteria) { walletAddress := gethcommon.HexToAddress(address) contractAddress := gethcommon.HexToAddress(criteria.ContractAddresses[chainID]) balance, ok := new(big.Int).SetString(criteria.Amount, 10) s.Require().True(ok) decimalsFactor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(criteria.Decimals)), nil) balance.Mul(balance, decimalsFactor) s.mockedBalances[chainID][walletAddress][contractAddress] = (*hexutil.Big)(balance) } func (s *MessengerCommunitiesTokenPermissionsSuite) waitOnKeyDistribution(condition func(*CommunityAndKeyActions) bool) <-chan error { testCommunitiesKeyDistributor, ok := s.owner.communitiesKeyDistributor.(*TestCommunitiesKeyDistributor) s.Require().True(ok) return testCommunitiesKeyDistributor.waitOnKeyDistribution(condition) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestCreateTokenPermission() { community, _ := s.createCommunity() createTokenPermission := &requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{uint64(testChainID1): "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), }, }, } response, err := s.owner.CreateCommunityTokenPermission(createTokenPermission) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) tokenPermissions := response.Communities()[0].TokenPermissions() for _, tokenPermission := range tokenPermissions { for _, tc := range tokenPermission.TokenCriteria { s.Require().Equal(tc.Type, protobuf.CommunityTokenType_ERC20) s.Require().Equal(tc.Symbol, "TEST") s.Require().Equal(tc.Amount, "100") s.Require().Equal(tc.Decimals, uint64(18)) } } } func (s *MessengerCommunitiesTokenPermissionsSuite) TestEditTokenPermission() { community, _ := s.createCommunity() tokenPermission := &requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), }, }, } response, err := s.owner.CreateCommunityTokenPermission(tokenPermission) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) tokenPermissions := response.Communities()[0].TokenPermissions() var tokenPermissionID string for id := range tokenPermissions { tokenPermissionID = id } tokenPermission.TokenCriteria[0].Symbol = "TESTUpdated" tokenPermission.TokenCriteria[0].Amount = "200" tokenPermission.TokenCriteria[0].Decimals = uint64(20) editTokenPermission := &requests.EditCommunityTokenPermission{ PermissionID: tokenPermissionID, CreateCommunityTokenPermission: *tokenPermission, } response2, err := s.owner.EditCommunityTokenPermission(editTokenPermission) s.Require().NoError(err) // wait for `checkMemberPermissions` to finish time.Sleep(1 * time.Second) s.Require().NotNil(response2) s.Require().Len(response2.Communities(), 1) tokenPermissions = response2.Communities()[0].TokenPermissions() for _, tokenPermission := range tokenPermissions { for _, tc := range tokenPermission.TokenCriteria { s.Require().Equal(tc.Type, protobuf.CommunityTokenType_ERC20) s.Require().Equal(tc.Symbol, "TESTUpdated") s.Require().Equal(tc.Amount, "200") s.Require().Equal(tc.Decimals, uint64(20)) } } } func (s *MessengerCommunitiesTokenPermissionsSuite) TestCommunityTokensMetadata() { community, _ := s.createCommunity() tokensMetadata := community.CommunityTokensMetadata() s.Require().Len(tokensMetadata, 0) newToken := &protobuf.CommunityTokenMetadata{ ContractAddresses: map[uint64]string{testChainID1: "0xasd"}, Description: "desc1", Image: "IMG1", TokenType: protobuf.CommunityTokenType_ERC721, Symbol: "SMB", Decimals: 3, } _, err := community.AddCommunityTokensMetadata(newToken) s.Require().NoError(err) tokensMetadata = community.CommunityTokensMetadata() s.Require().Len(tokensMetadata, 1) s.Require().Equal(tokensMetadata[0].ContractAddresses, newToken.ContractAddresses) s.Require().Equal(tokensMetadata[0].Description, newToken.Description) s.Require().Equal(tokensMetadata[0].Image, newToken.Image) s.Require().Equal(tokensMetadata[0].TokenType, newToken.TokenType) s.Require().Equal(tokensMetadata[0].Symbol, newToken.Symbol) s.Require().Equal(tokensMetadata[0].Name, newToken.Name) s.Require().Equal(tokensMetadata[0].Decimals, newToken.Decimals) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestRequestAccessWithENSTokenPermission() { community, _ := s.createCommunity() createTokenPermission := &requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ENS, EnsPattern: "test.stateofus.eth", }, }, } response, err := s.owner.CreateCommunityTokenPermission(createTokenPermission) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) s.advertiseCommunityTo(community, s.alice) requestToJoin := &requests.RequestToJoinCommunity{CommunityID: community.ID()} // We try to join the org response, err = s.alice.RequestToJoinCommunity(requestToJoin) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.RequestsToJoinCommunity, 1) requestToJoin1 := response.RequestsToJoinCommunity[0] s.Require().Equal(communities.RequestToJoinStatePending, requestToJoin1.State) // Retrieve request to join err = tt.RetryWithBackOff(func() error { response, err = s.owner.RetrieveAll() return err }) s.Require().NoError(err) // We don't expect a requestToJoin in the response because due // to missing revealed wallet addresses, the request should've // been declined right away s.Require().Len(response.RequestsToJoinCommunity, 0) // Ensure alice is not a member of the community allCommunities, err := s.owner.Communities() if bytes.Equal(allCommunities[0].ID(), community.ID()) { s.Require().False(allCommunities[0].HasMember(&s.alice.identity.PublicKey)) } } func (s *MessengerCommunitiesTokenPermissionsSuite) TestJoinedCommunityMembersSharedAddress() { community, _ := s.createCommunity() s.advertiseCommunityTo(community, s.alice) s.advertiseCommunityTo(community, s.bob) s.joinCommunity(community, s.alice, alicePassword, []string{}) s.joinCommunity(community, s.bob, bobPassword, []string{}) community, err := s.owner.GetCommunityByID(community.ID()) s.Require().NoError(err) s.Require().Equal(3, community.MembersCount()) for pubKey := range community.Members() { if pubKey != common.PubkeyToHex(&s.owner.identity.PublicKey) { revealedAccounts, err := s.owner.communitiesManager.GetRevealedAddresses(community.ID(), pubKey) s.Require().NoError(err) switch pubKey { case common.PubkeyToHex(&s.alice.identity.PublicKey): s.Require().Len(revealedAccounts, 2) s.Require().Equal(revealedAccounts[0].Address, aliceAddress1) s.Require().Equal(revealedAccounts[1].Address, aliceAddress2) s.Require().Equal(true, revealedAccounts[0].IsAirdropAddress) case common.PubkeyToHex(&s.bob.identity.PublicKey): s.Require().Len(revealedAccounts, 1) s.Require().Equal(revealedAccounts[0].Address, bobAddress) s.Require().Equal(true, revealedAccounts[0].IsAirdropAddress) default: s.Require().Fail("pubKey does not match expected keys") } } } } func (s *MessengerCommunitiesTokenPermissionsSuite) TestJoinedCommunityMembersSelectedSharedAddress() { community, _ := s.createCommunity() s.advertiseCommunityTo(community, s.alice) s.joinCommunity(community, s.alice, alicePassword, []string{aliceAddress2}) community, err := s.owner.GetCommunityByID(community.ID()) s.Require().NoError(err) s.Require().Equal(2, community.MembersCount()) for pubKey := range community.Members() { if pubKey != common.PubkeyToHex(&s.owner.identity.PublicKey) { revealedAccounts, err := s.owner.communitiesManager.GetRevealedAddresses(community.ID(), pubKey) s.Require().NoError(err) s.Require().Len(revealedAccounts, 1) switch pubKey { case common.PubkeyToHex(&s.alice.identity.PublicKey): s.Require().Equal(revealedAccounts[0].Address, aliceAddress2) s.Require().Equal(true, revealedAccounts[0].IsAirdropAddress) default: s.Require().Fail("pubKey does not match expected keys") } } } } func (s *MessengerCommunitiesTokenPermissionsSuite) TestJoinedCommunityMembersMultipleSelectedSharedAddresses() { community, _ := s.createCommunity() s.advertiseCommunityTo(community, s.alice) s.joinCommunityWithAirdropAddress(community, s.alice, alicePassword, []string{aliceAddress1, aliceAddress2}, aliceAddress2) community, err := s.owner.GetCommunityByID(community.ID()) s.Require().NoError(err) s.Require().Equal(2, community.MembersCount()) for pubKey := range community.Members() { if pubKey != common.PubkeyToHex(&s.owner.identity.PublicKey) { revealedAccounts, err := s.owner.communitiesManager.GetRevealedAddresses(community.ID(), pubKey) s.Require().NoError(err) s.Require().Len(revealedAccounts, 2) switch pubKey { case common.PubkeyToHex(&s.alice.identity.PublicKey): s.Require().Equal(revealedAccounts[0].Address, aliceAddress1) s.Require().Equal(revealedAccounts[1].Address, aliceAddress2) s.Require().Equal(true, revealedAccounts[1].IsAirdropAddress) default: s.Require().Fail("pubKey does not match expected keys") } } } } func (s *MessengerCommunitiesTokenPermissionsSuite) validateAddress(accounts []*protobuf.RevealedAccount, wantedAddress string) error { for _, account := range accounts { if account.Address != wantedAddress { return errors.New("revealed address does not match the wanted address. Wanted " + wantedAddress + ", Found: " + account.Address) } } return nil } func (s *MessengerCommunitiesTokenPermissionsSuite) TestEditSharedAddresses() { community, _ := s.createCommunity() s.advertiseCommunityTo(community, s.alice) s.joinCommunity(community, s.alice, alicePassword, []string{aliceAddress2}) community, err := s.owner.GetCommunityByID(community.ID()) s.Require().NoError(err) s.Require().Equal(2, community.MembersCount()) revealedAccounts, err := s.owner.communitiesManager.GetRevealedAddresses(community.ID(), common.PubkeyToHex(&s.alice.identity.PublicKey)) s.Require().NoError(err) err = s.validateAddress(revealedAccounts, aliceAddress2) s.Require().NoError(err) passwdHash := types.EncodeHex(crypto.Keccak256([]byte(alicePassword))) request := &requests.EditSharedAddresses{CommunityID: community.ID(), Password: passwdHash, AddressesToReveal: []string{aliceAddress1}, AirdropAddress: aliceAddress1} response, err := s.alice.EditSharedAddressesForCommunity(request) s.Require().NoError(err) s.Require().NotNil(response) // Retrieve address change err = tt.RetryWithBackOff(func() error { response, err := s.owner.RetrieveAll() if err != nil { return err } if len(response.Communities()) == 0 { return errors.New("no communities in response (address change reception)") } return nil }) s.Require().NoError(err) revealedAccounts, err = s.owner.communitiesManager.GetRevealedAddresses(community.ID(), common.PubkeyToHex(&s.alice.identity.PublicKey)) s.Require().NoError(err) err = s.validateAddress(revealedAccounts, aliceAddress1) s.Require().NoError(err) // Retrieve community description change err = tt.RetryWithBackOff(func() error { response, err := s.alice.RetrieveAll() if err != nil { return err } if len(response.Communities()) == 0 { return errors.New("no communities in response (address change reception)") } return nil }) s.Require().NoError(err) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestBecomeMemberPermissions() { community, chat := s.createCommunity() // bob joins the community s.advertiseCommunityTo(community, s.bob) s.joinCommunity(community, s.bob, bobPassword, []string{}) // send message to the channel msg := s.sendChatMessage(s.owner, chat.ID, "hello on open community") // bob can read the message response, err := WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { for _, message := range r.messages { if message.Text == msg.Text { return true } } return false }, "no messages", ) s.Require().NoError(err) s.Require().Len(response.Messages(), 1) s.Require().Equal(msg.Text, response.Messages()[0].Text) // setup become member permission permissionRequest := requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), }, }, } waitOnBobToBeKicked := waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { return len(sub.Community.Members()) == 1 }) waitOnCommunityToBeRekeyedOnceBobIsKicked := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool { return len(sub.community.Description().Members) == 1 && sub.keyActions.CommunityKeyAction.ActionType == communities.EncryptionKeyRekey }) response, err = s.owner.CreateCommunityTokenPermission(&permissionRequest) s.Require().NoError(err) s.Require().Len(response.Communities(), 1) err = <-waitOnBobToBeKicked s.Require().NoError(err) // bob should be kicked from the community, // because he doesn't meet the criteria community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().Len(community.Members(), 1) // bob receives community changes _, err = WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, "no community", ) s.Require().NoError(err) err = <-waitOnCommunityToBeRekeyedOnceBobIsKicked s.Require().NoError(err) // send message to channel msg = s.sendChatMessage(s.owner, chat.ID, "hello on encrypted community") // bob can't read the message _, err = WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { for _, message := range r.messages { if message.Text == msg.Text { return true } } return false }, "no messages", ) s.Require().Error(err) s.Require().ErrorContains(err, "no messages") // bob tries to join, but he doesn't satisfy so the request isn't sent passwdHash := types.EncodeHex(crypto.Keccak256([]byte(bobPassword))) request := &requests.RequestToJoinCommunity{CommunityID: community.ID(), Password: passwdHash, AddressesToReveal: []string{bobAddress}, AirdropAddress: bobAddress} _, err = s.bob.RequestToJoinCommunity(request) s.Require().Error(err) s.Require().ErrorContains(err, "permission to join not satisfied") // make sure bob does not have a pending request to join requests, err := s.bob.MyPendingRequestsToJoin() s.Require().NoError(err) s.Require().Len(requests, 0) // make bob satisfy the criteria s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, permissionRequest.TokenCriteria[0]) waitOnCommunityKeyToBeDistributedToBob := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool { return len(sub.community.Description().Members) == 2 && len(sub.keyActions.CommunityKeyAction.Members) == 1 && sub.keyActions.CommunityKeyAction.ActionType == communities.EncryptionKeySendToMembers }) // bob re-joins the community s.joinCommunity(community, s.bob, bobPassword, []string{}) err = <-waitOnCommunityKeyToBeDistributedToBob s.Require().NoError(err) // send message to channel msg = s.sendChatMessage(s.owner, chat.ID, "hello on encrypted community 2") // bob can read the message response, err = WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { for _, message := range r.messages { if message.Text == msg.Text { return true } } return false }, "no messages", ) s.Require().NoError(err) s.Require().Len(response.Messages(), 1) s.Require().Equal(msg.Text, response.Messages()[0].Text) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestJoinCommunityWithAdminPermission() { community, _ := s.createCommunity() // setup become admin permission permissionRequest := requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_ADMIN, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), }, }, } response, err := s.owner.CreateCommunityTokenPermission(&permissionRequest) s.Require().NoError(err) s.Require().Len(response.Communities(), 1) s.advertiseCommunityTo(community, s.bob) // Bob should still be able to join even if there is a permission to be an admin s.joinCommunity(community, s.bob, bobPassword, []string{}) // Verify that we have Bob's revealed account revealedAccounts, err := s.owner.GetRevealedAccounts(community.ID(), common.PubkeyToHex(&s.bob.identity.PublicKey)) s.Require().NoError(err) s.Require().Len(revealedAccounts, 1) s.Require().Equal(bobAddress, revealedAccounts[0].Address) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestJoinCommunityAsMemberWithMemberAndAdminPermission() { community, _ := s.createCommunity() // setup become member permission permissionRequestMember := requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), }, }, } response, err := s.owner.CreateCommunityTokenPermission(&permissionRequestMember) s.Require().NoError(err) s.Require().Len(response.Communities(), 1) // setup become admin permission permissionRequestAdmin := requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_ADMIN, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x124"}, Symbol: "TESTADMIN", Amount: "100", Decimals: uint64(18), }, }, } response, err = s.owner.CreateCommunityTokenPermission(&permissionRequestAdmin) s.Require().NoError(err) s.Require().Len(response.Communities(), 1) // make bob satisfy the member criteria s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, permissionRequestMember.TokenCriteria[0]) s.advertiseCommunityTo(community, s.bob) // Bob should still be able to join even though he doesn't satisfy the admin requirement // because he satisfies the member one s.joinCommunity(community, s.bob, bobPassword, []string{}) // Verify that we have Bob's revealed account revealedAccounts, err := s.owner.GetRevealedAccounts(community.ID(), common.PubkeyToHex(&s.bob.identity.PublicKey)) s.Require().NoError(err) s.Require().Len(revealedAccounts, 1) s.Require().Equal(bobAddress, revealedAccounts[0].Address) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestJoinCommunityAsAdminWithMemberAndAdminPermission() { community, _ := s.createCommunity() // setup become member permission permissionRequestMember := requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), }, }, } response, err := s.owner.CreateCommunityTokenPermission(&permissionRequestMember) s.Require().NoError(err) s.Require().Len(response.Communities(), 1) // setup become admin permission permissionRequestAdmin := requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_ADMIN, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x124"}, Symbol: "TESTADMIN", Amount: "100", Decimals: uint64(18), }, }, } response, err = s.owner.CreateCommunityTokenPermission(&permissionRequestAdmin) s.Require().NoError(err) s.Require().Len(response.Communities(), 1) // make bob satisfy the admin criteria s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, permissionRequestAdmin.TokenCriteria[0]) s.advertiseCommunityTo(community, s.bob) // Bob should still be able to join even though he doesn't satisfy the member requirement // because he satisfies the admin one s.joinCommunity(community, s.bob, bobPassword, []string{}) // Verify that we have Bob's revealed account revealedAccounts, err := s.owner.GetRevealedAccounts(community.ID(), common.PubkeyToHex(&s.bob.identity.PublicKey)) s.Require().NoError(err) s.Require().Len(revealedAccounts, 1) s.Require().Equal(bobAddress, revealedAccounts[0].Address) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions() { community, chat := s.createCommunity() // bob joins the community s.advertiseCommunityTo(community, s.bob) s.joinCommunity(community, s.bob, bobPassword, []string{}) // send message to the channel msg := s.sendChatMessage(s.owner, chat.ID, "hello on open community") // bob can read the message response, err := WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { for _, message := range r.messages { if message.Text == msg.Text { return true } } return false }, "no messages", ) s.Require().NoError(err) s.Require().Len(response.Messages(), 1) s.Require().Equal(msg.Text, response.Messages()[0].Text) // setup view channel permission channelPermissionRequest := requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), }, }, ChatIds: []string{chat.ID}, } waitOnBobToBeKickedFromChannel := waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { for channelID, channel := range sub.Community.Chats() { if channelID == chat.CommunityChatID() && len(channel.Members) == 1 { return true } } return false }) waitOnChannelToBeRekeyedOnceBobIsKicked := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool { for channelID, action := range sub.keyActions.ChannelKeysActions { if channelID == chat.CommunityChatID() && action.ActionType == communities.EncryptionKeyRekey { return true } } return false }) response, err = s.owner.CreateCommunityTokenPermission(&channelPermissionRequest) s.Require().NoError(err) s.Require().Len(response.Communities(), 1) s.Require().True(s.owner.communitiesManager.IsChannelEncrypted(community.IDString(), chat.ID)) err = <-waitOnBobToBeKickedFromChannel s.Require().NoError(err) err = <-waitOnChannelToBeRekeyedOnceBobIsKicked s.Require().NoError(err) // send message to the channel msg = s.sendChatMessage(s.owner, chat.ID, "hello on closed channel") // bob can't read the message _, err = WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { for _, message := range r.messages { if message.Text == msg.Text { return true } } return false }, "no messages", ) s.Require().Error(err) s.Require().ErrorContains(err, "no messages") // make bob satisfy channel criteria s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, channelPermissionRequest.TokenCriteria[0]) waitOnChannelKeyToBeDistributedToBob := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool { for channelID, action := range sub.keyActions.ChannelKeysActions { if channelID == chat.CommunityChatID() && action.ActionType == communities.EncryptionKeySendToMembers { for memberPubKey := range action.Members { if memberPubKey == common.PubkeyToHex(&s.bob.identity.PublicKey) { return true } } } } return false }) // force owner to reevaluate channel members // in production it will happen automatically, by periodic check community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) err = s.owner.communitiesManager.ReevaluateMembers(community) s.Require().NoError(err) err = <-waitOnChannelKeyToBeDistributedToBob s.Require().NoError(err) // ensure key is delivered to bob before message is sent // FIXME: this step shouldn't be necessary as we store hash ratchet messages // for later, to decrypt them when the key arrives. // for some reason, without it, the test is flaky _, _ = WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { return false }, "", ) // send message to the channel msg = s.sendChatMessage(s.owner, chat.ID, "hello on closed channel 2") // bob can read the message response, err = WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { for _, message := range r.messages { if message.Text == msg.Text { return true } } return false }, "no messages", ) s.Require().NoError(err) s.Require().Len(response.Messages(), 1) s.Require().Equal(msg.Text, response.Messages()[0].Text) } func (s *MessengerCommunitiesTokenPermissionsSuite) testReevaluateMemberPrivilegedRoleInOpenCommunity(permissionType protobuf.CommunityTokenPermission_Type) { community, _ := s.createCommunity() createTokenPermission := &requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: permissionType, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), }, }, } response, err := s.owner.CreateCommunityTokenPermission(createTokenPermission) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) s.Require().True(response.Communities()[0].HasTokenPermissions()) waitOnCommunityPermissionCreated := waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { return sub.Community.HasTokenPermissions() }) err = <-waitOnCommunityPermissionCreated s.Require().NoError(err) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(community.HasTokenPermissions()) s.advertiseCommunityTo(community, s.alice) var tokenPermission *communities.CommunityTokenPermission for _, tokenPermission = range community.TokenPermissions() { break } s.makeAddressSatisfyTheCriteria(testChainID1, aliceAddress1, tokenPermission.TokenCriteria[0]) // join community as a privileged user s.joinCommunity(community, s.alice, alicePassword, []string{aliceAddress1}) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(checkRoleBasedOnThePermissionType(permissionType, &s.alice.identity.PublicKey, community)) // the control node re-evaluates the roles of the participants, checking that the privileged user has not lost his role err = s.owner.communitiesManager.ReevaluateMembers(community) s.Require().NoError(err) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(checkRoleBasedOnThePermissionType(permissionType, &s.alice.identity.PublicKey, community)) // remove privileged token permission and reevaluate member permissions deleteTokenPermission := &requests.DeleteCommunityTokenPermission{ CommunityID: community.ID(), PermissionID: tokenPermission.Id, } response, err = s.owner.DeleteCommunityTokenPermission(deleteTokenPermission) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) s.Require().False(response.Communities()[0].HasTokenPermissions()) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().False(community.HasTokenPermissions()) err = s.owner.communitiesManager.ReevaluateMembers(community) s.Require().NoError(err) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(community.HasMember(&s.alice.identity.PublicKey)) s.Require().False(checkRoleBasedOnThePermissionType(permissionType, &s.alice.identity.PublicKey, community)) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestReevaluateMemberAdminRoleInOpenCommunity() { s.testReevaluateMemberPrivilegedRoleInOpenCommunity(protobuf.CommunityTokenPermission_BECOME_ADMIN) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestReevaluateMemberTokenMasterRoleInOpenCommunity() { s.testReevaluateMemberPrivilegedRoleInOpenCommunity(protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER) } func (s *MessengerCommunitiesTokenPermissionsSuite) testReevaluateMemberPrivilegedRoleInClosedCommunity(permissionType protobuf.CommunityTokenPermission_Type) { community, _ := s.createCommunity() createTokenPermission := &requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: permissionType, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), }, }, } response, err := s.owner.CreateCommunityTokenPermission(createTokenPermission) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) s.Require().True(response.Communities()[0].HasTokenPermissions()) createTokenMemberPermission := &requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, ContractAddresses: map[uint64]string{testChainID1: "0x124"}, Symbol: "TEST2", Amount: "100", Decimals: uint64(18), }, }, } waitOnCommunityPermissionCreated := waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { return len(sub.Community.TokenPermissions()) == 2 }) response, err = s.owner.CreateCommunityTokenPermission(createTokenMemberPermission) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) community = response.Communities()[0] s.Require().True(community.HasTokenPermissions()) s.Require().Len(community.TokenPermissions(), 2) err = <-waitOnCommunityPermissionCreated s.Require().NoError(err) s.advertiseCommunityTo(community, s.alice) var tokenPermission *communities.CommunityTokenPermission var tokenMemberPermission *communities.CommunityTokenPermission for _, permission := range community.TokenPermissions() { if permission.Type == protobuf.CommunityTokenPermission_BECOME_MEMBER { tokenMemberPermission = permission } else { tokenPermission = permission } } s.makeAddressSatisfyTheCriteria(testChainID1, aliceAddress1, tokenPermission.TokenCriteria[0]) s.makeAddressSatisfyTheCriteria(testChainID1, aliceAddress1, tokenMemberPermission.TokenCriteria[0]) // join community as a privileged user s.joinCommunity(community, s.alice, alicePassword, []string{aliceAddress1}) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(checkRoleBasedOnThePermissionType(permissionType, &s.alice.identity.PublicKey, community)) // the control node reevaluates the roles of the participants, checking that the privileged user has not lost his role err = s.owner.communitiesManager.ReevaluateMembers(community) s.Require().NoError(err) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(checkRoleBasedOnThePermissionType(permissionType, &s.alice.identity.PublicKey, community)) deleteTokenPermission := &requests.DeleteCommunityTokenPermission{ CommunityID: community.ID(), PermissionID: tokenPermission.Id, } // remove privileged token permission and reevaluate member permissions response, err = s.owner.DeleteCommunityTokenPermission(deleteTokenPermission) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) s.Require().Len(response.Communities()[0].TokenPermissions(), 1) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().Len(response.Communities()[0].TokenPermissions(), 1) err = s.owner.communitiesManager.ReevaluateMembers(community) s.Require().NoError(err) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(community.HasMember(&s.alice.identity.PublicKey)) s.Require().False(checkRoleBasedOnThePermissionType(permissionType, &s.alice.identity.PublicKey, community)) // delete member permissions and reevaluate user permissions deleteMemberTokenPermission := &requests.DeleteCommunityTokenPermission{ CommunityID: community.ID(), PermissionID: tokenMemberPermission.Id, } response, err = s.owner.DeleteCommunityTokenPermission(deleteMemberTokenPermission) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) s.Require().Len(response.Communities()[0].TokenPermissions(), 0) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().Len(response.Communities()[0].TokenPermissions(), 0) err = s.owner.communitiesManager.ReevaluateMembers(community) s.Require().NoError(err) community, err = s.owner.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(community.HasMember(&s.alice.identity.PublicKey)) s.Require().False(checkRoleBasedOnThePermissionType(permissionType, &s.alice.identity.PublicKey, community)) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestReevaluateMemberAdminRoleInClosedCommunity() { s.testReevaluateMemberPrivilegedRoleInClosedCommunity(protobuf.CommunityTokenPermission_BECOME_ADMIN) } func (s *MessengerCommunitiesTokenPermissionsSuite) TestReevaluateMemberTokenMasterRoleInClosedCommunity() { s.testReevaluateMemberPrivilegedRoleInClosedCommunity(protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER) } func checkRoleBasedOnThePermissionType(permissionType protobuf.CommunityTokenPermission_Type, member *ecdsa.PublicKey, community *communities.Community) bool { switch permissionType { case protobuf.CommunityTokenPermission_BECOME_ADMIN: return community.IsMemberAdmin(member) case protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER: return community.IsMemberTokenMaster(member) default: panic("Unknown permission, please, update the test") } }