From 19464eb345a85924042b096e29bdb6ff75eb8c9c Mon Sep 17 00:00:00 2001 From: Mykhailo Prakhov <117639195+mprakhov@users.noreply.github.com> Date: Mon, 27 Nov 2023 10:54:46 +0100 Subject: [PATCH] feat: show activity center notification if user must reveal addressed to join/rejoin the community (#4373) - show activity center notification if user must reveal addressed to join/rejoin the community - fixed unit test, added validation that ex-owner receive AC notification --- protocol/activity_center.go | 1 + protocol/communities/errors.go | 2 + protocol/communities/persistence_test.go | 53 +++++++ .../communities_messenger_signers_test.go | 146 +++++++++++------- protocol/messenger_communities.go | 45 ++++++ 5 files changed, 189 insertions(+), 58 deletions(-) diff --git a/protocol/activity_center.go b/protocol/activity_center.go index c6a8a9f86..a78c00867 100644 --- a/protocol/activity_center.go +++ b/protocol/activity_center.go @@ -34,6 +34,7 @@ const ( ActivityCenterNotificationTypeOwnershipLost ActivityCenterNotificationTypeSetSignerFailed ActivityCenterNotificationTypeSetSignerDeclined + ActivityCenterNotificationTypeShareAccounts ) type ActivityCenterMembershipStatus int diff --git a/protocol/communities/errors.go b/protocol/communities/errors.go index f03d08665..3ab6fb343 100644 --- a/protocol/communities/errors.go +++ b/protocol/communities/errors.go @@ -41,3 +41,5 @@ var ErrNotEnoughPermissions = errors.New("not enough permissions for this commun var ErrCannotRemoveOwnerOrAdmin = errors.New("not allowed to remove admin or owner") var ErrCannotBanOwnerOrAdmin = errors.New("not allowed to ban admin or owner") var ErrInvalidManageTokensPermission = errors.New("no privileges to manage tokens") +var ErrRevealedAccountsAbsent = errors.New("revealed accounts is absent") +var ErrNoRevealedAccountsSignature = errors.New("revealed accounts without the signature") diff --git a/protocol/communities/persistence_test.go b/protocol/communities/persistence_test.go index 401e0f3a8..2e5d93745 100644 --- a/protocol/communities/persistence_test.go +++ b/protocol/communities/persistence_test.go @@ -666,3 +666,56 @@ func (s *PersistenceSuite) TestCuratedCommunities() { s.Require().NoError(err) s.Require().True(reflect.DeepEqual(communities, setCommunities)) } + +func (s *PersistenceSuite) TestGetCommunityRequestToJoinWithRevealedAddresses() { + identity, err := crypto.GenerateKey() + s.Require().NoError(err, "crypto.GenerateKey shouldn't give any error") + + clock := uint64(time.Now().Unix()) + communityID := types.HexBytes{7, 7, 7, 7, 7, 7, 7, 7} + revealedAddresses := []string{"address1", "address2", "address3"} + chainIds := []uint64{1, 2} + publicKey := common.PubkeyToHex(&identity.PublicKey) + signature := []byte("test") + + // No data in database + _, err = s.db.GetCommunityRequestToJoinWithRevealedAddresses(publicKey, communityID) + s.Require().ErrorIs(err, sql.ErrNoRows) + + // RTJ with 2 withoutRevealed Addresses + expectedRtj := &RequestToJoin{ + ID: types.HexBytes{1, 2, 3, 4, 5, 6, 7, 8}, + PublicKey: publicKey, + Clock: clock, + CommunityID: communityID, + State: RequestToJoinStateAccepted, + RevealedAccounts: []*protobuf.RevealedAccount{ + { + Address: revealedAddresses[2], + ChainIds: chainIds, + IsAirdropAddress: true, + Signature: signature, + }, + }, + } + err = s.db.SaveRequestToJoin(expectedRtj) + s.Require().NoError(err, "SaveRequestToJoin shouldn't give any error") + + // check that there will be no error if revealed account is absent + rtjResult, err := s.db.GetCommunityRequestToJoinWithRevealedAddresses(publicKey, communityID) + s.Require().NoError(err, "RevealedAccounts empty, shouldn't give any error") + + s.Require().Len(rtjResult.RevealedAccounts, 0) + + // save revealed accounts for previous request to join + err = s.db.SaveRequestToJoinRevealedAddresses(expectedRtj.ID, expectedRtj.RevealedAccounts) + s.Require().NoError(err) + + rtjResult, err = s.db.GetCommunityRequestToJoinWithRevealedAddresses(publicKey, communityID) + s.Require().NoError(err) + s.Require().Equal(expectedRtj.ID, rtjResult.ID) + s.Require().Equal(expectedRtj.PublicKey, rtjResult.PublicKey) + s.Require().Equal(expectedRtj.Clock, rtjResult.Clock) + s.Require().Equal(expectedRtj.CommunityID, rtjResult.CommunityID) + s.Require().Len(rtjResult.RevealedAccounts, 1) +} diff --git a/protocol/communities_messenger_signers_test.go b/protocol/communities_messenger_signers_test.go index f15bcfdc2..b39cba4fc 100644 --- a/protocol/communities_messenger_signers_test.go +++ b/protocol/communities_messenger_signers_test.go @@ -2,13 +2,15 @@ package protocol import ( "context" - "crypto/ecdsa" "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" @@ -37,6 +39,10 @@ type MessengerCommunitiesSignersSuite struct { logger *zap.Logger collectiblesServiceMock *CollectiblesServiceMock + + accountsTestData map[string]string + + mockedBalances map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big // chainID, account, token, balance } func (s *MessengerCommunitiesSignersSuite) SetupTest() { @@ -53,15 +59,23 @@ func (s *MessengerCommunitiesSignersSuite) SetupTest() { s.shh = gethbridge.NewGethWakuWrapper(shh) s.Require().NoError(shh.Start()) - s.john = s.newMessenger() - s.bob = s.newMessenger() - s.alice = s.newMessenger() + aliceAccountAddress := "0x0777100000000000000000000000000000000000" + bobAccountAddress := "0x0330000000000000000000000000000000000000" + accountPassword := "QWERTY" + + s.john = s.newMessenger("", []string{}) + s.bob = s.newMessenger(accountPassword, []string{aliceAccountAddress}) + s.alice = s.newMessenger(accountPassword, []string{bobAccountAddress}) _, 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)] = bobAccountAddress + s.accountsTestData[common.PubkeyToHex(&s.alice.identity.PublicKey)] = aliceAccountAddress } func (s *MessengerCommunitiesSignersSuite) TearDownTest() { @@ -71,18 +85,25 @@ func (s *MessengerCommunitiesSignersSuite) TearDownTest() { _ = s.logger.Sync() } -func (s *MessengerCommunitiesSignersSuite) newMessengerWithKey(privateKey *ecdsa.PrivateKey) *Messenger { - messenger, err := newCommunitiesTestMessenger(s.shh, privateKey, s.logger, nil, nil, s.collectiblesServiceMock) - s.Require().NoError(err) - - return messenger -} - -func (s *MessengerCommunitiesSignersSuite) newMessenger() *Messenger { +func (s *MessengerCommunitiesSignersSuite) newMessenger(password string, walletAddresses []string) *Messenger { privateKey, err := crypto.GenerateKey() s.Require().NoError(err) - return s.newMessengerWithKey(privateKey) + accountsManagerMock := &AccountManagerMock{} + accountsManagerMock.AccountsMap = make(map[string]string) + + for _, walletAddress := range walletAddresses { + accountsManagerMock.AccountsMap[walletAddress] = types.EncodeHex(crypto.Keccak256([]byte(password))) + } + + tokenManagerMock := &TokenManagerMock{ + Balances: &s.mockedBalances, + } + + messenger, err := newCommunitiesTestMessenger(s.shh, privateKey, s.logger, accountsManagerMock, tokenManagerMock, s.collectiblesServiceMock) + s.Require().NoError(err) + + return messenger } func (s *MessengerCommunitiesSignersSuite) createCommunity(controlNode *Messenger) *communities.Community { @@ -95,18 +116,40 @@ func (s *MessengerCommunitiesSignersSuite) advertiseCommunityTo(controlNode *Mes } func (s *MessengerCommunitiesSignersSuite) joinCommunity(controlNode *Messenger, community *communities.Community, user *Messenger) { - request := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + 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) { - request := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + 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}, + } + joinOnRequestCommunity(&s.Suite, community, controlNode, user, request) } // John crates a community // Ownership is transferred to Alice -// Alice kick all members Bob and John rejoins +// 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() { @@ -193,19 +236,26 @@ func (s *MessengerCommunitiesSignersSuite) TestControlNodeUpdateSigner() { ) s.Require().NoError(err) - // check that John received kick event, also he will receive - // request to share RevealedAddresses and send request to join to the control node + // 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 { - return len(r.Communities()) > 0 && !r.Communities()[0].HasMember(&s.john.identity.PublicKey) + 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 - // TODO: please, check TODO's in this test and uncomment them if Members() == 3 _, err = WaitOnMessengerResponse( s.alice, func(r *MessengerResponse) bool { @@ -243,20 +293,8 @@ func (s *MessengerCommunitiesSignersSuite) TestControlNodeUpdateSigner() { s.Require().False(community.IsControlNode()) s.Require().False(community.IsOwner()) - // TODO: uncomment when ex-owner will start sharing request to join with revealed address - // // Jonh 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) - // }, - // "John was auto-accepted", - // ) - // s.Require().NoError(err) - - // community = validateResults(s.john) - // 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 @@ -290,15 +328,14 @@ func (s *MessengerCommunitiesSignersSuite) TestControlNodeUpdateSigner() { validateNameInDB(s.alice) - // TODO: uncomment when ex-owner will start sharing request to join with revealed address // 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) + _, 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( @@ -394,14 +431,21 @@ func (s *MessengerCommunitiesSignersSuite) TestAutoAcceptOnOwnershipChangeReques _, err = WaitOnSignaledMessengerResponse( s.john, func(r *MessengerResponse) bool { - return len(r.Communities()) > 0 && !r.Communities()[0].HasMember(&s.john.identity.PublicKey) + 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 - // TODO: please, check TODO's in this test and uncomment them if Members() == 3 _, err = WaitOnMessengerResponse( s.alice, func(r *MessengerResponse) bool { @@ -437,20 +481,6 @@ func (s *MessengerCommunitiesSignersSuite) TestAutoAcceptOnOwnershipChangeReques community = validateResults(s.bob) s.Require().False(community.IsControlNode()) s.Require().False(community.IsOwner()) - - // TODO: uncomment when ex-owner will start sharing request to join with revealed address - // _, err = WaitOnMessengerResponse( - // s.john, - // func(r *MessengerResponse) bool { - // return len(r.Communities()) > 0 && r.Communities()[0].HasMember(&s.bob.identity.PublicKey) - // }, - // "John was auto-accepted", - // ) - // s.Require().NoError(err) - - // community = validateResults(s.john) - // s.Require().False(community.IsControlNode()) - // s.Require().False(community.IsOwner()) } func (s *MessengerCommunitiesSignersSuite) TestNewOwnerAcceptRequestToJoin() { diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index 5457c4808..0a3425ed7 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -3,6 +3,7 @@ package protocol import ( "context" "crypto/ecdsa" + "database/sql" "errors" "fmt" "math" @@ -392,8 +393,13 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti // control node changed and we were kicked out. It now awaits our addresses if communityResponse.Changes.ControlNodeChanged != nil && communityResponse.Changes.MemberKicked { requestToJoin, err := m.sendSharedAddressToControlNode(communityResponse.Community.ControlNode(), communityResponse.Community) + if err != nil { m.logger.Error("share address to control node failed", zap.String("id", types.EncodeHex(communityResponse.Community.ID())), zap.Error(err)) + + if err == communities.ErrRevealedAccountsAbsent || err == communities.ErrNoRevealedAccountsSignature { + m.AddActivityCenterNotificationToResponse(communityResponse.Community.IDString(), ActivityCenterNotificationTypeShareAccounts, response) + } } else { state.Response.RequestsToJoinCommunity = append(state.Response.RequestsToJoinCommunity, requestToJoin) } @@ -3105,9 +3111,30 @@ func (m *Messenger) sendSharedAddressToControlNode(receiver *ecdsa.PublicKey, co requestToJoin, err := m.communitiesManager.GetCommunityRequestToJoinWithRevealedAddresses(pk, community.ID()) if err != nil { + if err == sql.ErrNoRows { + return nil, communities.ErrRevealedAccountsAbsent + } return nil, err } + if len(requestToJoin.RevealedAccounts) == 0 { + return nil, communities.ErrRevealedAccountsAbsent + } + + // check if at least one account is signed + // old community users can not keep locally the signature of their revealed accounts in the DB + revealedAccountSigned := false + for _, account := range requestToJoin.RevealedAccounts { + revealedAccountSigned = len(account.Signature) > 0 + if revealedAccountSigned { + break + } + } + + if !revealedAccountSigned { + return nil, communities.ErrNoRevealedAccountsSignature + } + requestToJoin.Clock = uint64(time.Now().Unix()) requestToJoin.State = communities.RequestToJoinStateAwaitingAddresses payload, err := proto.Marshal(requestToJoin.ToCommunityRequestToJoinProtobuf()) @@ -4217,3 +4244,21 @@ func (m *Messenger) SendMessageToControlNode(community *communities.Community, r return m.sender.SendCommunityMessage(context.Background(), rawMessage) } + +func (m *Messenger) AddActivityCenterNotificationToResponse(communityID string, acType ActivityCenterType, response *MessengerResponse) { + // Activity Center notification + notification := &ActivityCenterNotification{ + ID: types.FromHex(uuid.New().String()), + Type: acType, + Timestamp: m.getTimesource().GetCurrentTime(), + CommunityID: communityID, + Read: false, + Deleted: false, + UpdatedAt: m.GetCurrentTimeInMillis(), + } + + err := m.addActivityCenterNotification(response, notification, nil) + if err != nil { + m.logger.Error("failed to save notification", zap.Error(err)) + } +}