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
This commit is contained in:
Mykhailo Prakhov 2023-11-27 10:54:46 +01:00 committed by GitHub
parent 8641ec5dd5
commit 19464eb345
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 189 additions and 58 deletions

View File

@ -34,6 +34,7 @@ const (
ActivityCenterNotificationTypeOwnershipLost ActivityCenterNotificationTypeOwnershipLost
ActivityCenterNotificationTypeSetSignerFailed ActivityCenterNotificationTypeSetSignerFailed
ActivityCenterNotificationTypeSetSignerDeclined ActivityCenterNotificationTypeSetSignerDeclined
ActivityCenterNotificationTypeShareAccounts
) )
type ActivityCenterMembershipStatus int type ActivityCenterMembershipStatus int

View File

@ -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 ErrCannotRemoveOwnerOrAdmin = errors.New("not allowed to remove admin or owner")
var ErrCannotBanOwnerOrAdmin = errors.New("not allowed to ban admin or owner") var ErrCannotBanOwnerOrAdmin = errors.New("not allowed to ban admin or owner")
var ErrInvalidManageTokensPermission = errors.New("no privileges to manage tokens") 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")

View File

@ -666,3 +666,56 @@ func (s *PersistenceSuite) TestCuratedCommunities() {
s.Require().NoError(err) s.Require().NoError(err)
s.Require().True(reflect.DeepEqual(communities, setCommunities)) 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)
}

View File

@ -2,13 +2,15 @@ package protocol
import ( import (
"context" "context"
"crypto/ecdsa"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"go.uber.org/zap" "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" 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/crypto"
"github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/eth-node/types"
@ -37,6 +39,10 @@ type MessengerCommunitiesSignersSuite struct {
logger *zap.Logger logger *zap.Logger
collectiblesServiceMock *CollectiblesServiceMock 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() { func (s *MessengerCommunitiesSignersSuite) SetupTest() {
@ -53,15 +59,23 @@ func (s *MessengerCommunitiesSignersSuite) SetupTest() {
s.shh = gethbridge.NewGethWakuWrapper(shh) s.shh = gethbridge.NewGethWakuWrapper(shh)
s.Require().NoError(shh.Start()) s.Require().NoError(shh.Start())
s.john = s.newMessenger() aliceAccountAddress := "0x0777100000000000000000000000000000000000"
s.bob = s.newMessenger() bobAccountAddress := "0x0330000000000000000000000000000000000000"
s.alice = s.newMessenger() 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() _, err := s.john.Start()
s.Require().NoError(err) s.Require().NoError(err)
_, err = s.bob.Start() _, err = s.bob.Start()
s.Require().NoError(err) s.Require().NoError(err)
_, err = s.alice.Start() _, err = s.alice.Start()
s.Require().NoError(err) 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() { func (s *MessengerCommunitiesSignersSuite) TearDownTest() {
@ -71,18 +85,25 @@ func (s *MessengerCommunitiesSignersSuite) TearDownTest() {
_ = s.logger.Sync() _ = s.logger.Sync()
} }
func (s *MessengerCommunitiesSignersSuite) newMessengerWithKey(privateKey *ecdsa.PrivateKey) *Messenger { func (s *MessengerCommunitiesSignersSuite) newMessenger(password string, walletAddresses []string) *Messenger {
messenger, err := newCommunitiesTestMessenger(s.shh, privateKey, s.logger, nil, nil, s.collectiblesServiceMock)
s.Require().NoError(err)
return messenger
}
func (s *MessengerCommunitiesSignersSuite) newMessenger() *Messenger {
privateKey, err := crypto.GenerateKey() privateKey, err := crypto.GenerateKey()
s.Require().NoError(err) 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 { 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) { 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, "") joinCommunity(&s.Suite, community, controlNode, user, request, "")
} }
func (s *MessengerCommunitiesSignersSuite) joinOnRequestCommunity(controlNode *Messenger, community *communities.Community, user *Messenger) { 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) joinOnRequestCommunity(&s.Suite, community, controlNode, user, request)
} }
// John crates a community // John crates a community
// Ownership is transferred to Alice // 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 // Bob and John accepts the changes
func (s *MessengerCommunitiesSignersSuite) TestControlNodeUpdateSigner() { func (s *MessengerCommunitiesSignersSuite) TestControlNodeUpdateSigner() {
@ -193,19 +236,26 @@ func (s *MessengerCommunitiesSignersSuite) TestControlNodeUpdateSigner() {
) )
s.Require().NoError(err) s.Require().NoError(err)
// check that John received kick event, also he will receive // check that John received kick event, and AC notification msg created
// request to share RevealedAddresses and send request to join to the control node // John, as ex-owner must manually join the community
_, err = WaitOnSignaledMessengerResponse( _, err = WaitOnSignaledMessengerResponse(
s.john, s.john,
func(r *MessengerResponse) bool { 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", "John was not kicked from the community",
) )
s.Require().NoError(err) s.Require().NoError(err)
// Alice auto-accept requests to join with RevealedAddresses // Alice auto-accept requests to join with RevealedAddresses
// TODO: please, check TODO's in this test and uncomment them if Members() == 3
_, err = WaitOnMessengerResponse( _, err = WaitOnMessengerResponse(
s.alice, s.alice,
func(r *MessengerResponse) bool { func(r *MessengerResponse) bool {
@ -243,20 +293,8 @@ func (s *MessengerCommunitiesSignersSuite) TestControlNodeUpdateSigner() {
s.Require().False(community.IsControlNode()) s.Require().False(community.IsControlNode())
s.Require().False(community.IsOwner()) s.Require().False(community.IsOwner())
// TODO: uncomment when ex-owner will start sharing request to join with revealed address // John manually joins the community
// // Jonh is a community member again s.joinCommunity(s.alice, community, s.john)
// _, 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())
// Alice change community name // Alice change community name
@ -290,15 +328,14 @@ func (s *MessengerCommunitiesSignersSuite) TestControlNodeUpdateSigner() {
validateNameInDB(s.alice) 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) // john accepts community update from alice (new control node)
// _, err = WaitOnMessengerResponse( _, err = WaitOnMessengerResponse(
// s.john, s.john,
// validateNameInResponse, validateNameInResponse,
// "john did not receive community name update", "john did not receive community name update",
// ) )
// s.Require().NoError(err) s.Require().NoError(err)
// validateNameInDB(s.john) validateNameInDB(s.john)
// bob accepts community update from alice (new control node) // bob accepts community update from alice (new control node)
_, err = WaitOnMessengerResponse( _, err = WaitOnMessengerResponse(
@ -394,14 +431,21 @@ func (s *MessengerCommunitiesSignersSuite) TestAutoAcceptOnOwnershipChangeReques
_, err = WaitOnSignaledMessengerResponse( _, err = WaitOnSignaledMessengerResponse(
s.john, s.john,
func(r *MessengerResponse) bool { 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", "John was not kicked from the community",
) )
s.Require().NoError(err) s.Require().NoError(err)
// Alice auto-accept requests to join with RevealedAddresses // Alice auto-accept requests to join with RevealedAddresses
// TODO: please, check TODO's in this test and uncomment them if Members() == 3
_, err = WaitOnMessengerResponse( _, err = WaitOnMessengerResponse(
s.alice, s.alice,
func(r *MessengerResponse) bool { func(r *MessengerResponse) bool {
@ -437,20 +481,6 @@ func (s *MessengerCommunitiesSignersSuite) TestAutoAcceptOnOwnershipChangeReques
community = validateResults(s.bob) community = validateResults(s.bob)
s.Require().False(community.IsControlNode()) s.Require().False(community.IsControlNode())
s.Require().False(community.IsOwner()) 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() { func (s *MessengerCommunitiesSignersSuite) TestNewOwnerAcceptRequestToJoin() {

View File

@ -3,6 +3,7 @@ package protocol
import ( import (
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"database/sql"
"errors" "errors"
"fmt" "fmt"
"math" "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 // control node changed and we were kicked out. It now awaits our addresses
if communityResponse.Changes.ControlNodeChanged != nil && communityResponse.Changes.MemberKicked { if communityResponse.Changes.ControlNodeChanged != nil && communityResponse.Changes.MemberKicked {
requestToJoin, err := m.sendSharedAddressToControlNode(communityResponse.Community.ControlNode(), communityResponse.Community) requestToJoin, err := m.sendSharedAddressToControlNode(communityResponse.Community.ControlNode(), communityResponse.Community)
if err != nil { if err != nil {
m.logger.Error("share address to control node failed", zap.String("id", types.EncodeHex(communityResponse.Community.ID())), zap.Error(err)) 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 { } else {
state.Response.RequestsToJoinCommunity = append(state.Response.RequestsToJoinCommunity, requestToJoin) 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()) requestToJoin, err := m.communitiesManager.GetCommunityRequestToJoinWithRevealedAddresses(pk, community.ID())
if err != nil { if err != nil {
if err == sql.ErrNoRows {
return nil, communities.ErrRevealedAccountsAbsent
}
return nil, err 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.Clock = uint64(time.Now().Unix())
requestToJoin.State = communities.RequestToJoinStateAwaitingAddresses requestToJoin.State = communities.RequestToJoinStateAwaitingAddresses
payload, err := proto.Marshal(requestToJoin.ToCommunityRequestToJoinProtobuf()) 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) 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))
}
}