package protocol import ( "bytes" "context" "testing" "time" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/suite" "go.uber.org/zap" gethcommon "github.com/ethereum/go-ethereum/common" hexutil "github.com/ethereum/go-ethereum/common/hexutil" utils "github.com/status-im/status-go/common" gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" "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/communities/token" "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/services/wallet/bigint" "github.com/status-im/status-go/services/wallet/thirdparty" "github.com/status-im/status-go/waku" ) func TestMessengerCommunitiesSignersSuite(t *testing.T) { suite.Run(t, new(MessengerCommunitiesSignersSuite)) } type MessengerCommunitiesSignersSuite struct { suite.Suite john *Messenger bob *Messenger alice *Messenger shh types.Waku logger *zap.Logger collectiblesServiceMock *CollectiblesServiceMock accountsTestData map[string]string mockedBalances communities.BalancesByChain mockedCollectibles communities.CollectiblesByChain } func (s *MessengerCommunitiesSignersSuite) SetupTest() { communities.SetValidateInterval(300 * time.Millisecond) s.logger = tt.MustCreateTestLogger() s.collectiblesServiceMock = &CollectiblesServiceMock{} config := waku.DefaultConfig config.MinimumAcceptedPoW = 0 shh := waku.New(&config, s.logger) s.shh = gethbridge.NewGethWakuWrapper(shh) s.Require().NoError(shh.Start()) accountPassword := "QWERTY" s.mockedBalances = make(communities.BalancesByChain) 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(bobAddress)] = make(map[gethcommon.Address]*hexutil.Big) s.mockedCollectibles = make(communities.CollectiblesByChain) s.mockedCollectibles[testChainID1] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress) s.mockedCollectibles[testChainID1][gethcommon.HexToAddress(aliceAddress1)] = make(thirdparty.TokenBalancesPerContractAddress) s.mockedCollectibles[testChainID1][gethcommon.HexToAddress(bobAddress)] = make(thirdparty.TokenBalancesPerContractAddress) s.john = s.newMessenger("", []string{}) s.bob = s.newMessenger(accountPassword, []string{aliceAddress1}) s.alice = s.newMessenger(accountPassword, []string{bobAddress}) _, err := s.john.Start() s.Require().NoError(err) _, err = s.bob.Start() s.Require().NoError(err) _, err = s.alice.Start() s.Require().NoError(err) s.accountsTestData = make(map[string]string) s.accountsTestData[common.PubkeyToHex(&s.bob.identity.PublicKey)] = bobAddress s.accountsTestData[common.PubkeyToHex(&s.alice.identity.PublicKey)] = aliceAddress1 } func (s *MessengerCommunitiesSignersSuite) TearDownTest() { TearDownMessenger(&s.Suite, s.john) TearDownMessenger(&s.Suite, s.bob) TearDownMessenger(&s.Suite, s.alice) _ = s.logger.Sync() } func (s *MessengerCommunitiesSignersSuite) newMessenger(password string, walletAddresses []string) *Messenger { communityManagerOptions := []communities.ManagerOption{ communities.WithAllowForcingCommunityMembersReevaluation(true), } return newTestCommunitiesMessenger(&s.Suite, s.shh, testCommunitiesMessengerConfig{ testMessengerConfig: testMessengerConfig{ logger: s.logger, extraOptions: []Option{WithCommunityManagerOptions(communityManagerOptions)}, }, password: password, walletAddresses: walletAddresses, mockedBalances: &s.mockedBalances, mockedCollectibles: &s.mockedCollectibles, collectiblesService: s.collectiblesServiceMock, }) } func (s *MessengerCommunitiesSignersSuite) createCommunity(controlNode *Messenger) *communities.Community { community, _ := createCommunity(&s.Suite, controlNode) return community } func (s *MessengerCommunitiesSignersSuite) advertiseCommunityTo(controlNode *Messenger, community *communities.Community, user *Messenger) { advertiseCommunityTo(&s.Suite, community, controlNode, user) } func (s *MessengerCommunitiesSignersSuite) joinCommunity(controlNode *Messenger, community *communities.Community, user *Messenger) { accTestData := s.accountsTestData[common.PubkeyToHex(&s.alice.identity.PublicKey)] array64Bytes := common.HashPublicKey(&s.alice.identity.PublicKey) signature := append([]byte{0}, array64Bytes...) request := &requests.RequestToJoinCommunity{ CommunityID: community.ID(), AddressesToReveal: []string{accTestData}, AirdropAddress: accTestData, Signatures: []types.HexBytes{signature}, } joinCommunity(&s.Suite, community, controlNode, user, request, "") } func (s *MessengerCommunitiesSignersSuite) joinOnRequestCommunity(controlNode *Messenger, community *communities.Community, user *Messenger) { accTestData := s.accountsTestData[common.PubkeyToHex(&user.identity.PublicKey)] array64Bytes := common.HashPublicKey(&user.identity.PublicKey) signature := append([]byte{0}, array64Bytes...) request := &requests.RequestToJoinCommunity{ CommunityID: community.ID(), AddressesToReveal: []string{accTestData}, AirdropAddress: accTestData, Signatures: []types.HexBytes{signature}, } joinOnRequestCommunity(&s.Suite, community, controlNode, user, request) } func (s *MessengerCommunitiesSignersSuite) makeAddressSatisfyTheCriteria(chainID uint64, address string, criteria *protobuf.TokenCriteria) { makeAddressSatisfyTheCriteria(&s.Suite, s.mockedBalances, s.mockedCollectibles, chainID, address, criteria) } // John crates a community // Ownership is transferred to Alice // Alice kick all members Bob and John // Bob automatically rejoin // John receive AC notification to share the address and join to the community // Bob and John accepts the changes func (s *MessengerCommunitiesSignersSuite) TestControlNodeUpdateSigner() { // Create a community // Transfer ownership // Process message community := s.createCommunity(s.john) s.advertiseCommunityTo(s.john, community, s.bob) s.advertiseCommunityTo(s.john, community, s.alice) s.joinCommunity(s.john, community, s.bob) s.joinCommunity(s.john, community, s.alice) // john mints owner token var chainID uint64 = 1 tokenAddress := "token-address" tokenName := "tokenName" tokenSymbol := "TSM" _, err := s.john.SaveCommunityToken(&token.CommunityToken{ TokenType: protobuf.CommunityTokenType_ERC721, CommunityID: community.IDString(), Address: tokenAddress, ChainID: int(chainID), Name: tokenName, Supply: &bigint.BigInt{}, Symbol: tokenSymbol, PrivilegesLevel: token.OwnerLevel, }, nil) s.Require().NoError(err) // john adds minted owner token to community err = s.john.AddCommunityToken(community.IDString(), int(chainID), tokenAddress) s.Require().NoError(err) // update mock - the signer for the community returned by the contracts should be john s.collectiblesServiceMock.SetSignerPubkeyForCommunity(community.ID(), common.PubkeyToHex(&s.john.identity.PublicKey)) s.collectiblesServiceMock.SetMockCollectibleContractData(chainID, tokenAddress, &communities.CollectibleContractData{TotalSupply: &bigint.BigInt{}}) // bob accepts community update _, err = WaitOnSignaledMessengerResponse( s.bob, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && len(r.Communities()[0].TokenPermissions()) == 1 }, "no communities", ) s.Require().NoError(err) // alice accepts community update _, err = WaitOnSignaledMessengerResponse( s.alice, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && len(r.Communities()[0].TokenPermissions()) == 1 }, "no communities", ) s.Require().NoError(err) // Ownership token will be transferred to Alice and she will kick all members // and request kicked members to rejoin // the signer for the community returned by the contracts should be alice s.collectiblesServiceMock.SetSignerPubkeyForCommunity(community.ID(), common.PubkeyToHex(&s.alice.identity.PublicKey)) response, err := s.alice.PromoteSelfToControlNode(community.ID()) s.Require().NoError(err) s.Require().NotNil(response) community, err = s.alice.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(community.IsControlNode()) s.Require().True(common.IsPubKeyEqual(community.ControlNode(), &s.alice.identity.PublicKey)) s.Require().True(community.IsOwner()) // check that Bob received kick event, also he will receive // request to share RevealedAddresses and send request to join to the control node _, err = WaitOnSignaledMessengerResponse( s.bob, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && !r.Communities()[0].HasMember(&s.bob.identity.PublicKey) && !r.Communities()[0].Joined() && r.Communities()[0].Spectated() && len(r.ActivityCenterNotifications()) == 0 }, "Bob was not kicked from the community", ) s.Require().NoError(err) // check that John received kick event, and AC notification msg created // John, as ex-owner must manually join the community _, err = WaitOnSignaledMessengerResponse( s.john, func(r *MessengerResponse) bool { inSpectateMode := len(r.Communities()) > 0 && !r.Communities()[0].HasMember(&s.john.identity.PublicKey) && !r.Communities()[0].Joined() && r.Communities()[0].Spectated() sharedNotificationExist := false for _, acNotification := range r.ActivityCenterNotifications() { if acNotification.Type == ActivityCenterNotificationTypeShareAccounts { sharedNotificationExist = true break } } return inSpectateMode && sharedNotificationExist }, "John was not kicked from the community", ) s.Require().NoError(err) // Alice auto-accept requests to join with RevealedAddresses _, err = WaitOnMessengerResponse( s.alice, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && len(r.Communities()[0].Members()) == 2 }, "no community update with accepted request", ) s.Require().NoError(err) validateResults := func(messenger *Messenger) *communities.Community { community, err = messenger.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(common.IsPubKeyEqual(community.ControlNode(), &s.alice.identity.PublicKey)) s.Require().Len(community.Members(), 2) s.Require().True(community.HasMember(&messenger.identity.PublicKey)) return community } community = validateResults(s.alice) s.Require().True(community.IsControlNode()) s.Require().True(community.IsOwner()) // Bob is a community member again _, err = WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && r.Communities()[0].HasMember(&s.bob.identity.PublicKey) && r.Communities()[0].Joined() && !r.Communities()[0].Spectated() }, "Bob was auto-accepted", ) s.Require().NoError(err) community = validateResults(s.bob) s.Require().False(community.IsControlNode()) s.Require().False(community.IsOwner()) // John manually joins the community s.joinCommunity(s.alice, community, s.john) // Alice change community name expectedName := "Alice owns community" response, err = s.alice.EditCommunity(&requests.EditCommunity{ CommunityID: community.ID(), CreateCommunity: requests.CreateCommunity{ Membership: protobuf.CommunityPermissions_AUTO_ACCEPT, Name: expectedName, Color: "#000000", Description: "edited community description", }, }) s.Require().NoError(err) s.Require().NotNil(response) validateNameInResponse := func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && r.Communities()[0].IDString() == community.IDString() && r.Communities()[0].Name() == expectedName } s.Require().True(validateNameInResponse(response)) validateNameInDB := func(messenger *Messenger) { community, err = messenger.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().Equal(expectedName, response.Communities()[0].Name()) } validateNameInDB(s.alice) // john accepts community update from alice (new control node) _, err = WaitOnMessengerResponse( s.john, validateNameInResponse, "john did not receive community name update", ) s.Require().NoError(err) validateNameInDB(s.john) // bob accepts community update from alice (new control node) _, err = WaitOnMessengerResponse( s.bob, validateNameInResponse, "bob did not receive community name update", ) s.Require().NoError(err) validateNameInDB(s.bob) } func (s *MessengerCommunitiesSignersSuite) TestAutoAcceptOnOwnershipChangeRequestRequired() { community, _ := createOnRequestCommunity(&s.Suite, s.john) s.advertiseCommunityTo(s.john, community, s.bob) s.advertiseCommunityTo(s.john, community, s.alice) s.joinOnRequestCommunity(s.john, community, s.bob) s.joinOnRequestCommunity(s.john, community, s.alice) // john mints owner token var chainID uint64 = 1 tokenAddress := "token-address" tokenName := "tokenName" tokenSymbol := "TSM" _, err := s.john.SaveCommunityToken(&token.CommunityToken{ TokenType: protobuf.CommunityTokenType_ERC721, CommunityID: community.IDString(), Address: tokenAddress, ChainID: int(chainID), Name: tokenName, Supply: &bigint.BigInt{}, Symbol: tokenSymbol, PrivilegesLevel: token.OwnerLevel, }, nil) s.Require().NoError(err) err = s.john.AddCommunityToken(community.IDString(), int(chainID), tokenAddress) s.Require().NoError(err) // set john as contract owner s.collectiblesServiceMock.SetSignerPubkeyForCommunity(community.ID(), common.PubkeyToHex(&s.john.identity.PublicKey)) s.collectiblesServiceMock.SetMockCollectibleContractData(chainID, tokenAddress, &communities.CollectibleContractData{TotalSupply: &bigint.BigInt{}}) hasTokenPermission := func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && r.Communities()[0].HasTokenPermissions() } // bob received owner permissions _, err = WaitOnSignaledMessengerResponse( s.bob, hasTokenPermission, "no communities with token permission for Bob", ) s.Require().NoError(err) // alice received owner permissions _, err = WaitOnSignaledMessengerResponse( s.alice, hasTokenPermission, "no communities with token permission for Alice", ) s.Require().NoError(err) // simulate Alice received owner token s.collectiblesServiceMock.SetSignerPubkeyForCommunity(community.ID(), common.PubkeyToHex(&s.alice.identity.PublicKey)) // after receiving owner token - set up control node, set up owner role, kick all members // and request kicked members to rejoin response, err := s.alice.PromoteSelfToControlNode(community.ID()) s.Require().NoError(err) s.Require().NotNil(response) community, err = s.alice.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(community.IsControlNode()) s.Require().True(common.IsPubKeyEqual(community.ControlNode(), &s.alice.identity.PublicKey)) s.Require().True(community.IsOwner()) // check that client received kick event // Bob will receive request to share RevealedAddresses and send request to join to the control node _, err = WaitOnSignaledMessengerResponse( s.bob, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && !r.Communities()[0].HasMember(&s.bob.identity.PublicKey) }, "Bob was not kicked from the community", ) s.Require().NoError(err) // check that client received kick event // John will receive request to share RevealedAddresses and send request to join to the control node _, err = WaitOnSignaledMessengerResponse( s.john, func(r *MessengerResponse) bool { wasKicked := len(r.Communities()) > 0 && !r.Communities()[0].HasMember(&s.john.identity.PublicKey) sharedNotificationExist := false for _, acNotification := range r.ActivityCenterNotifications() { if acNotification.Type == ActivityCenterNotificationTypeShareAccounts { sharedNotificationExist = true break } } return wasKicked && sharedNotificationExist }, "John was not kicked from the community", ) s.Require().NoError(err) // Alice auto-accept requests to join with RevealedAddresses _, err = WaitOnMessengerResponse( s.alice, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && len(r.Communities()[0].Members()) == 2 }, "no community update with accepted request", ) s.Require().NoError(err) validateResults := func(messenger *Messenger) *communities.Community { community, err = messenger.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(common.IsPubKeyEqual(community.ControlNode(), &s.alice.identity.PublicKey)) s.Require().Len(community.Members(), 2) s.Require().True(community.HasMember(&messenger.identity.PublicKey)) return community } community = validateResults(s.alice) s.Require().True(community.IsControlNode()) s.Require().True(community.IsOwner()) _, err = WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && r.Communities()[0].HasMember(&s.bob.identity.PublicKey) }, "Bob was auto-accepted", ) s.Require().NoError(err) community = validateResults(s.bob) s.Require().False(community.IsControlNode()) s.Require().False(community.IsOwner()) } func (s *MessengerCommunitiesSignersSuite) TestNewOwnerAcceptRequestToJoin() { // Create a community // Transfer ownership // New owner accepts new request to join community := s.createCommunity(s.john) s.advertiseCommunityTo(s.john, community, s.alice) s.joinCommunity(s.john, community, s.alice) // john mints owner token var chainID uint64 = 1 tokenAddress := "token-address" tokenName := "tokenName" tokenSymbol := "TSM" _, err := s.john.SaveCommunityToken(&token.CommunityToken{ TokenType: protobuf.CommunityTokenType_ERC721, CommunityID: community.IDString(), Address: tokenAddress, ChainID: int(chainID), Name: tokenName, Supply: &bigint.BigInt{}, Symbol: tokenSymbol, PrivilegesLevel: token.OwnerLevel, }, nil) s.Require().NoError(err) // john adds minted owner token to community err = s.john.AddCommunityToken(community.IDString(), int(chainID), tokenAddress) s.Require().NoError(err) // update mock - the signer for the community returned by the contracts should be john s.collectiblesServiceMock.SetSignerPubkeyForCommunity(community.ID(), common.PubkeyToHex(&s.john.identity.PublicKey)) s.collectiblesServiceMock.SetMockCollectibleContractData(chainID, tokenAddress, &communities.CollectibleContractData{TotalSupply: &bigint.BigInt{}}) // alice accepts community update _, err = WaitOnSignaledMessengerResponse( s.alice, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && len(r.Communities()[0].TokenPermissions()) == 1 }, "no communities", ) s.Require().NoError(err) // Ownership token will be transferred to Alice and she will kick all members // and request kicked members to rejoin // the signer for the community returned by the contracts should be alice s.collectiblesServiceMock.SetSignerPubkeyForCommunity(community.ID(), common.PubkeyToHex(&s.alice.identity.PublicKey)) response, err := s.alice.PromoteSelfToControlNode(community.ID()) s.Require().NoError(err) s.Require().NotNil(response) community, err = s.alice.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(community.IsControlNode()) s.Require().True(common.IsPubKeyEqual(community.ControlNode(), &s.alice.identity.PublicKey)) s.Require().True(community.IsOwner()) // check that John received kick event, also he will receive // request to share RevealedAddresses and send request to join to the control node _, err = WaitOnSignaledMessengerResponse( s.john, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && !r.Communities()[0].HasMember(&s.john.identity.PublicKey) }, "John was not kicked from the community", ) s.Require().NoError(err) // Alice advertises community to Bob chat := CreateOneToOneChat(common.PubkeyToHex(&s.bob.identity.PublicKey), &s.bob.identity.PublicKey, s.bob.transport) inputMessage := common.NewMessage() inputMessage.ChatId = chat.ID inputMessage.Text = "some text" inputMessage.CommunityID = community.IDString() err = s.alice.SaveChat(chat) s.Require().NoError(err) _, err = s.alice.SendChatMessage(context.Background(), inputMessage) s.Require().NoError(err) _, err = WaitOnSignaledMessengerResponse( s.bob, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, "Community was not advertised to Bob", ) s.Require().NoError(err) // Bob joins the community s.joinCommunity(s.alice, community, s.bob) } func (s *MessengerCommunitiesSignersSuite) testDescriptionSignature(description []byte) { var amm protobuf.ApplicationMetadataMessage err := proto.Unmarshal(description, &amm) s.Require().NoError(err) signer, err := utils.RecoverKey(&amm) s.Require().NoError(err) s.NotNil(signer) } func (s *MessengerCommunitiesSignersSuite) forceCommunityChange(community *communities.Community, owner *Messenger, user *Messenger) { newDescription := community.DescriptionText() + " new" _, err := owner.EditCommunity(&requests.EditCommunity{ CommunityID: community.ID(), CreateCommunity: requests.CreateCommunity{ Membership: protobuf.CommunityPermissions_AUTO_ACCEPT, Name: community.Name(), Color: community.Color(), Description: newDescription, }, }) s.Require().NoError(err) // alice receives new description _, err = WaitOnMessengerResponse(user, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && r.Communities()[0].DescriptionText() == newDescription }, "new description not received") s.Require().NoError(err) } func (s *MessengerCommunitiesSignersSuite) testSyncCommunity(mintOwnerToken bool) { community := s.createCommunity(s.john) s.advertiseCommunityTo(s.john, community, s.alice) s.joinCommunity(s.john, community, s.alice) // FIXME: Remove this workaround when fixed: // https://github.com/status-im/status-go/issues/4413 s.forceCommunityChange(community, s.john, s.alice) aliceCommunity, err := s.alice.GetCommunityByID(community.ID()) s.Require().NoError(err) s.testDescriptionSignature(aliceCommunity.DescriptionProtocolMessage()) if mintOwnerToken { // john mints owner token var chainID uint64 = 1 tokenAddress := "token-address" tokenName := "tokenName" tokenSymbol := "TSM" communityToken := &token.CommunityToken{ TokenType: protobuf.CommunityTokenType_ERC721, CommunityID: community.IDString(), Address: tokenAddress, ChainID: int(chainID), Name: tokenName, Supply: &bigint.BigInt{}, Symbol: tokenSymbol, PrivilegesLevel: token.OwnerLevel, } _, err := s.john.SaveCommunityToken(communityToken, nil) s.Require().NoError(err) // john adds minted owner token to community err = s.john.AddCommunityToken(community.IDString(), int(chainID), tokenAddress) s.Require().NoError(err) // update mock - the signer for the community returned by the contracts should be john s.collectiblesServiceMock.SetSignerPubkeyForCommunity(community.ID(), common.PubkeyToHex(&s.john.identity.PublicKey)) s.collectiblesServiceMock.SetMockCommunityTokenData(communityToken) // alice accepts community update _, err = WaitOnSignaledMessengerResponse( s.alice, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && len(r.Communities()[0].TokenPermissions()) == 1 }, "no communities", ) s.Require().NoError(err) } // Create alice second instance alice2, err := newMessengerWithKey( s.shh, s.alice.identity, s.logger.With(zap.String("name", "alice-2")), []Option{WithCommunityTokensService(s.collectiblesServiceMock)}) s.Require().NoError(err) defer TearDownMessenger(&s.Suite, alice2) // Create communities backup clock, _ := s.alice.getLastClockWithRelatedChat() communitiesBackup, err := s.alice.backupCommunities(context.Background(), clock) s.Require().NoError(err) // Find wanted communities in the backup var syncCommunityMessages []*protobuf.SyncInstallationCommunity for _, b := range communitiesBackup { for _, c := range b.Communities { if bytes.Equal(c.Id, community.ID()) { syncCommunityMessages = append(syncCommunityMessages, c) } } } s.Require().Len(syncCommunityMessages, 1) s.testDescriptionSignature(syncCommunityMessages[0].Description) // Push the backup into second instance messageState := alice2.buildMessageState() err = alice2.HandleSyncInstallationCommunity(messageState, syncCommunityMessages[0], nil) s.Require().NoError(err) s.Require().Len(messageState.Response.Communities(), 1) expectedControlNode := community.PublicKey() if mintOwnerToken { expectedControlNode = &s.john.identity.PublicKey } responseCommunity := messageState.Response.Communities()[0] s.Require().Equal(community.IDString(), responseCommunity.IDString()) s.Require().True(common.IsPubKeyEqual(expectedControlNode, responseCommunity.ControlNode())) } func (s *MessengerCommunitiesSignersSuite) TestSyncTokenGatedCommunity() { testCases := []struct { name string mintOwnerToken bool }{ { name: "general community sync", mintOwnerToken: false, }, { name: "community with token ownership", mintOwnerToken: true, }, } for _, tc := range testCases { s.Run(tc.name, func() { s.testSyncCommunity(tc.mintOwnerToken) }) } } func (s *MessengerCommunitiesSignersSuite) TestWithMintedOwnerTokenApplyCommunityEventsUponMakingDeviceControlNode() { community := s.createCommunity(s.john) // john mints owner token var chainID uint64 = 1 tokenAddress := "token-address" tokenName := "tokenName" tokenSymbol := "TSM" _, err := s.john.SaveCommunityToken(&token.CommunityToken{ TokenType: protobuf.CommunityTokenType_ERC721, CommunityID: community.IDString(), Address: tokenAddress, ChainID: int(chainID), Name: tokenName, Supply: &bigint.BigInt{}, Symbol: tokenSymbol, PrivilegesLevel: token.OwnerLevel, }, nil) s.Require().NoError(err) err = s.john.AddCommunityToken(community.IDString(), int(chainID), tokenAddress) s.Require().NoError(err) // Make sure there is no control node s.Require().False(common.IsPubKeyEqual(community.ControlNode(), &s.john.identity.PublicKey)) // Trick. We need to remove the community private key otherwise the events // will be signed and Events will be approved instead of being in Pending State. _, err = s.john.RemovePrivateKey(community.ID()) s.Require().NoError(err) request := 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", AmountInWei: "100000000000000000000", Decimals: uint64(18), }, }, } response, err := s.john.CreateCommunityTokenPermission(&request) s.Require().NoError(err) s.Require().Len(response.CommunityChanges, 1) s.Require().Len(response.CommunityChanges[0].TokenPermissionsAdded, 1) addedPermission := func() *communities.CommunityTokenPermission { for _, permission := range response.CommunityChanges[0].TokenPermissionsAdded { return permission } return nil }() s.Require().NotNil(addedPermission) s.Require().Equal(communities.TokenPermissionAdditionPending, addedPermission.State) messengerReponse, err := s.john.PromoteSelfToControlNode(community.ID()) s.Require().NoError(err) s.Require().Len(messengerReponse.Communities(), 1) tokenPermissions := messengerReponse.Communities()[0].TokenPermissions() s.Require().Len(tokenPermissions, 2) tokenPermissionsMap := make(map[protobuf.CommunityTokenPermission_Type]struct{}, len(tokenPermissions)) for _, t := range tokenPermissions { tokenPermissionsMap[t.Type] = struct{}{} } s.Require().Len(tokenPermissionsMap, 2) s.Require().Contains(tokenPermissionsMap, protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER) s.Require().Contains(tokenPermissionsMap, protobuf.CommunityTokenPermission_BECOME_ADMIN) for _, v := range tokenPermissions { s.Require().Equal(communities.TokenPermissionApproved, v.State) } } func (s *MessengerCommunitiesSignersSuite) TestWithoutMintedOwnerTokenMakingDeviceControlNodeIsBlocked() { community := s.createCommunity(s.john) // Make sure there is no control node s.Require().False(common.IsPubKeyEqual(community.ControlNode(), &s.john.identity.PublicKey)) response, err := s.john.PromoteSelfToControlNode(community.ID()) s.Require().Nil(response) s.Require().NotNil(err) s.Require().Error(err, "Owner token is needed") } func (s *MessengerCommunitiesSignersSuite) TestControlNodeDeviceChanged() { // Note: we don't have any specific check if control node device changed, // so in this test we will just call twice 'PromoteSelfToControlNode' community, _ := createOnRequestCommunity(&s.Suite, s.john) // john mints owner token ownerTokenAddress := "token-address" _, err := s.john.SaveCommunityToken(&token.CommunityToken{ TokenType: protobuf.CommunityTokenType_ERC721, CommunityID: community.IDString(), Address: ownerTokenAddress, ChainID: int(testChainID1), Name: "ownerToken", Supply: &bigint.BigInt{}, Symbol: "OT", PrivilegesLevel: token.OwnerLevel, }, nil) s.Require().NoError(err) err = s.john.AddCommunityToken(community.IDString(), int(testChainID1), ownerTokenAddress) s.Require().NoError(err) // john mints TM token tokenMasterTokenAddress := "token-master-address" _, err = s.john.SaveCommunityToken(&token.CommunityToken{ TokenType: protobuf.CommunityTokenType_ERC721, CommunityID: community.IDString(), Address: tokenMasterTokenAddress, ChainID: int(testChainID1), Name: "tokenMasterToken", Supply: &bigint.BigInt{}, Symbol: "TMT", PrivilegesLevel: token.MasterLevel, }, nil) s.Require().NoError(err) err = s.john.AddCommunityToken(community.IDString(), int(testChainID1), tokenMasterTokenAddress) s.Require().NoError(err) // set john as contract owner s.collectiblesServiceMock.SetSignerPubkeyForCommunity(community.ID(), common.PubkeyToHex(&s.john.identity.PublicKey)) s.collectiblesServiceMock.SetMockCollectibleContractData(testChainID1, ownerTokenAddress, &communities.CollectibleContractData{TotalSupply: &bigint.BigInt{}}) s.collectiblesServiceMock.SetMockCollectibleContractData(testChainID1, tokenMasterTokenAddress, &communities.CollectibleContractData{TotalSupply: &bigint.BigInt{}}) community, err = s.john.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(common.IsPubKeyEqual(community.ControlNode(), &s.john.identity.PublicKey)) var tokenMasterTokenCriteria *protobuf.TokenCriteria for _, permission := range community.TokenPermissions() { if permission.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER { s.Require().Len(permission.TokenCriteria, 1) tokenMasterTokenCriteria = permission.TokenCriteria[0] break } } s.Require().NotNil(tokenMasterTokenCriteria) s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, tokenMasterTokenCriteria) waitOnAliceCommunityValidation := waitOnCommunitiesEvent(s.alice, func(sub *communities.Subscription) bool { return sub.TokenCommunityValidated != nil }) s.advertiseCommunityTo(s.john, community, s.alice) err = <-waitOnAliceCommunityValidation s.Require().NoError(err) waitOnBobCommunityValidation := waitOnCommunitiesEvent(s.bob, func(sub *communities.Subscription) bool { return sub.TokenCommunityValidated != nil }) s.advertiseCommunityTo(s.john, community, s.bob) err = <-waitOnBobCommunityValidation s.Require().NoError(err) s.joinOnRequestCommunity(s.john, community, s.alice) s.joinOnRequestCommunity(s.john, community, s.bob) community, err = s.john.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().True(checkRoleBasedOnThePermissionType(protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER, &s.bob.identity.PublicKey, community)) // Simulate control node device changed and new control node device has all members revealed addresses response, err := s.john.PromoteSelfToControlNode(community.ID()) s.Require().NoError(err) s.Require().Len(response.CommunityChanges, 1) s.Require().Len(response.CommunityChanges[0].MembersRemoved, 0) // Simulate control node device changed and new control node device does not have bob's revealed addresses bobRequestID := communities.CalculateRequestID(s.bob.IdentityPublicKeyString(), community.ID()) err = s.john.communitiesManager.RemoveRequestToJoinRevealedAddresses(bobRequestID) s.Require().NoError(err) // due to test execution is fast, we update request to join clock clock := uint64(time.Now().Unix() - 2) err = s.john.communitiesManager.UpdateClockInRequestToJoin(bobRequestID, clock) s.Require().NoError(err) _, err = s.john.PromoteSelfToControlNode(community.ID()) s.Require().NoError(err) community, err = s.john.communitiesManager.GetByID(community.ID()) s.Require().NoError(err) s.Require().Len(community.Members(), 2) for _, chat := range community.Chats() { s.Require().Len(chat.Members, 2) } // Bob will receive request to share RevealedAddresses and send request to join to the control node _, err = WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { return len(r.Communities()) == 1 && !r.Communities()[0].HasMember(&s.bob.identity.PublicKey) && r.Communities()[0].Spectated() && len(r.ActivityCenterNotifications()) == 0 }, "Bob was not soft kicked from the community", ) s.Require().NoError(err) // check that alice was not soft kicked _, err = WaitOnMessengerResponse( s.alice, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && r.Communities()[0].HasMember(&s.alice.identity.PublicKey) && !r.Communities()[0].Spectated() && len(r.ActivityCenterNotifications()) == 0 && r.Communities()[0].Joined() }, "Alice was kicked from the community", ) s.Require().NoError(err) // John auto-accept requests to join with RevealedAddresses _, err = WaitOnMessengerResponse( s.john, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && len(r.Communities()[0].Members()) == 3 }, "no community update with accepted request", ) s.Require().NoError(err) _, err = WaitOnMessengerResponse( s.bob, func(r *MessengerResponse) bool { return len(r.Communities()) > 0 && r.Communities()[0].HasMember(&s.bob.identity.PublicKey) && r.Communities()[0].Joined() && !r.Communities()[0].Spectated() && r.Communities()[0].IsTokenMaster() }, "Bob was auto-accepted", ) s.Require().NoError(err) }