status-im/status-go

View on GitHub
protocol/communities_messenger_signers_test.go

Summary

Maintainability
A
3 hrs
Test Coverage
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)
}