status-go/protocol/communities_messenger_helpe...

555 lines
19 KiB
Go

package protocol
import (
"context"
"crypto/ecdsa"
"encoding/json"
"errors"
"sync"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/suite"
"go.uber.org/zap"
gethcommon "github.com/ethereum/go-ethereum/common"
hexutil "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/account/generator"
"github.com/status-im/status-go/appdatabase"
"github.com/status-im/status-go/common/dbsetup"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/services/communitytokens"
walletToken "github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/t/helpers"
"github.com/status-im/status-go/transactions"
"github.com/status-im/status-go/walletdatabase"
)
type AccountManagerMock struct {
AccountsMap map[string]string
}
func (m *AccountManagerMock) GetVerifiedWalletAccount(db *accounts.Database, address, password string) (*account.SelectedExtKey, error) {
return &account.SelectedExtKey{
Address: types.HexToAddress(address),
}, nil
}
func (m *AccountManagerMock) CanRecover(rpcParams account.RecoverParams, revealedAddress types.Address) (bool, error) {
return true, nil
}
func (m *AccountManagerMock) Sign(rpcParams account.SignParams, verifiedAccount *account.SelectedExtKey) (result types.HexBytes, err error) {
return types.HexBytes{}, nil
}
func (m *AccountManagerMock) DeleteAccount(address types.Address) error {
return nil
}
type TokenManagerMock struct {
Balances *map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big
}
func (m *TokenManagerMock) GetAllChainIDs() ([]uint64, error) {
chainIDs := make([]uint64, 0, len(*m.Balances))
for key := range *m.Balances {
chainIDs = append(chainIDs, key)
}
return chainIDs, nil
}
func (m *TokenManagerMock) GetBalancesByChain(ctx context.Context, accounts, tokenAddresses []gethcommon.Address, chainIDs []uint64) (map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big, error) {
time.Sleep(100 * time.Millisecond) // simulate response time
return *m.Balances, nil
}
func (m *TokenManagerMock) FindOrCreateTokenByAddress(ctx context.Context, chainID uint64, address gethcommon.Address) *walletToken.Token {
time.Sleep(100 * time.Millisecond) // simulate response time
return nil
}
type CollectiblesServiceMock struct {
Collectibles map[uint64]map[string]*communitytokens.CollectibleContractData
Assets map[uint64]map[string]*communitytokens.AssetContractData
Signers map[string]string
}
func (c *CollectiblesServiceMock) SetSignerPubkeyForCommunity(communityID []byte, signerPubKey string) {
if c.Signers == nil {
c.Signers = make(map[string]string)
}
c.Signers[types.EncodeHex(communityID)] = signerPubKey
}
func (c *CollectiblesServiceMock) SetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string, txArgs transactions.SendTxArgs, password string, newSignerPubKey string) (string, error) {
return "", nil
}
func (c *CollectiblesServiceMock) GetCollectibleContractData(chainID uint64, contractAddress string) (*communitytokens.CollectibleContractData, error) {
collectibleContractData, dataExists := c.Collectibles[chainID][contractAddress]
if dataExists {
return collectibleContractData, nil
}
return nil, nil
}
func (c *CollectiblesServiceMock) GetAssetContractData(chainID uint64, contractAddress string) (*communitytokens.AssetContractData, error) {
assetsContractData, dataExists := c.Assets[chainID][contractAddress]
if dataExists {
return assetsContractData, nil
}
return nil, nil
}
func (c *CollectiblesServiceMock) SetMockCollectibleContractData(chainID uint64, contractAddress string, collectible *communitytokens.CollectibleContractData) {
if c.Collectibles == nil {
c.Collectibles = make(map[uint64]map[string]*communitytokens.CollectibleContractData)
}
c.Collectibles[chainID] = make(map[string]*communitytokens.CollectibleContractData)
c.Collectibles[chainID][contractAddress] = collectible
}
func (c *CollectiblesServiceMock) SafeGetSignerPubKey(ctx context.Context, chainID uint64, communityID string) (string, error) {
if c.Signers == nil {
c.Signers = make(map[string]string)
}
return c.Signers[communityID], nil
}
func (c *CollectiblesServiceMock) SetMockAssetContractData(chainID uint64, contractAddress string, assetData *communitytokens.AssetContractData) {
if c.Assets == nil {
c.Assets = make(map[uint64]map[string]*communitytokens.AssetContractData)
}
c.Assets[chainID] = make(map[string]*communitytokens.AssetContractData)
c.Assets[chainID][contractAddress] = assetData
}
func (c *CollectiblesServiceMock) DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error) {
return gethcommon.Hex2Bytes("ccbb375343347491706cf4b43796f7b96ccc89c9e191a8b78679daeba1684ec7"), nil
}
func newMessenger(s *suite.Suite, shh types.Waku, logger *zap.Logger, password string, walletAddresses []string,
mockedBalances *map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big, collectiblesService communitytokens.ServiceInterface) *Messenger {
accountsManagerMock := &AccountManagerMock{}
accountsManagerMock.AccountsMap = make(map[string]string)
for _, walletAddress := range walletAddresses {
accountsManagerMock.AccountsMap[walletAddress] = types.EncodeHex(crypto.Keccak256([]byte(password)))
}
tokenManagerMock := &TokenManagerMock{
Balances: mockedBalances,
}
privateKey, err := crypto.GenerateKey()
s.Require().NoError(err)
messenger, err := newCommunitiesTestMessenger(shh, privateKey, logger, accountsManagerMock, tokenManagerMock, collectiblesService)
s.Require().NoError(err)
currentDistributorObj, ok := messenger.communitiesKeyDistributor.(*CommunitiesKeyDistributorImpl)
s.Require().True(ok)
messenger.communitiesKeyDistributor = &TestCommunitiesKeyDistributor{
CommunitiesKeyDistributorImpl: *currentDistributorObj,
subscriptions: map[chan *CommunityAndKeyActions]bool{},
mutex: sync.RWMutex{},
}
// add wallet account with keypair
for _, walletAddress := range walletAddresses {
kp := accounts.GetProfileKeypairForTest(false, true, false)
kp.Accounts[0].Address = types.HexToAddress(walletAddress)
err := messenger.settings.SaveOrUpdateKeypair(kp)
s.Require().NoError(err)
}
walletAccounts, err := messenger.settings.GetActiveAccounts()
s.Require().NoError(err)
s.Require().Len(walletAccounts, len(walletAddresses))
for i := range walletAddresses {
s.Require().Equal(walletAccounts[i].Type, accounts.AccountTypeGenerated)
}
return messenger
}
func newCommunitiesTestMessenger(shh types.Waku, privateKey *ecdsa.PrivateKey, logger *zap.Logger, accountsManager account.Manager,
tokenManager communities.TokenManager, collectiblesService communitytokens.ServiceInterface) (*Messenger, error) {
madb, err := multiaccounts.InitializeDB(dbsetup.InMemoryPath)
if err != nil {
return nil, err
}
acc := generator.NewAccount(privateKey, nil)
iai := acc.ToIdentifiedAccountInfo("")
walletDb, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{})
if err != nil {
return nil, err
}
appDb, err := helpers.SetupTestMemorySQLDB(appdatabase.DbInitializer{})
if err != nil {
return nil, err
}
options := []Option{
WithCustomLogger(logger),
WithDatabase(appDb),
WithWalletDatabase(walletDb),
WithMultiAccounts(madb),
WithAccount(iai.ToMultiAccount()),
WithDatasync(),
WithResendParams(3, 3),
WithTokenManager(tokenManager),
}
if collectiblesService != nil {
options = append(options, WithCommunityTokensService(collectiblesService))
}
m, err := NewMessenger(
"Test",
privateKey,
&testNode{shh: shh},
uuid.New().String(),
nil,
accountsManager,
options...,
)
if err != nil {
return nil, err
}
err = m.Init()
if err != nil {
return nil, err
}
config := params.NodeConfig{
NetworkID: 10,
DataDir: "test",
}
networks := json.RawMessage("{}")
setting := settings.Settings{
Address: types.HexToAddress("0x1122334455667788990011223344556677889900"),
AnonMetricsShouldSend: false,
CurrentNetwork: "mainnet_rpc",
DappsAddress: types.HexToAddress("0x1122334455667788990011223344556677889900"),
InstallationID: "d3efcff6-cffa-560e-a547-21d3858cbc51",
KeyUID: "0x1122334455667788990011223344556677889900",
Name: "Test",
Networks: &networks,
LatestDerivedPath: 0,
PhotoPath: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAjklEQVR4nOzXwQmFMBAAUZXUYh32ZB32ZB02sxYQQSZGsod55/91WFgSS0RM+SyjA56ZRZhFmEWYRRT6h+M6G16zrxv6fdJpmUWYRbxsYr13dKfanpN0WmYRZhGzXz6AWYRZRIfbaX26fT9Jk07LLMIsosPt9I/dTDotswizCG+nhFmEWYRZhFnEHQAA///z1CFkYamgfQAAAABJRU5ErkJggg==",
PreviewPrivacy: false,
PublicKey: "0x04112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900",
SigningPhrase: "yurt joey vibe",
SendPushNotifications: true,
ProfilePicturesVisibility: 1,
DefaultSyncPeriod: 777600,
UseMailservers: true,
LinkPreviewRequestEnabled: true,
SendStatusUpdates: true,
WalletRootAddress: types.HexToAddress("0x1122334455667788990011223344556677889900")}
_ = m.settings.CreateSettings(setting, config)
return m, nil
}
func createCommunity(s *suite.Suite, owner *Messenger) (*communities.Community, *Chat) {
return createCommunityConfigurable(s, owner, protobuf.CommunityPermissions_AUTO_ACCEPT)
}
func createOnRequestCommunity(s *suite.Suite, owner *Messenger) (*communities.Community, *Chat) {
return createCommunityConfigurable(s, owner, protobuf.CommunityPermissions_MANUAL_ACCEPT)
}
func createCommunityConfigurable(s *suite.Suite, owner *Messenger, permission protobuf.CommunityPermissions_Access) (*communities.Community, *Chat) {
description := &requests.CreateCommunity{
Membership: protobuf.CommunityPermissions_AUTO_ACCEPT,
Name: "status",
Color: "#ffffff",
Description: "status community description",
}
// Create an community chat
response, err := owner.CreateCommunity(description, true)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Communities(), 1)
community := response.Communities()[0]
s.Require().True(community.Joined())
s.Require().True(community.IsControlNode())
s.Require().Len(response.CommunitiesSettings(), 1)
communitySettings := response.CommunitiesSettings()[0]
s.Require().Equal(communitySettings.CommunityID, community.IDString())
s.Require().Equal(communitySettings.HistoryArchiveSupportEnabled, false)
orgChat := &protobuf.CommunityChat{
Permissions: &protobuf.CommunityPermissions{
Access: protobuf.CommunityPermissions_AUTO_ACCEPT,
},
Identity: &protobuf.ChatIdentity{
DisplayName: "status-core",
Emoji: "😎",
Description: "status-core community chat",
},
}
response, err = owner.CreateCommunityChat(community.ID(), orgChat)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Chats(), 1)
return community, response.Chats()[0]
}
func advertiseCommunityTo(s *suite.Suite, community *communities.Community, owner *Messenger, user *Messenger) {
// Create wrapped (Signed) community data.
wrappedCommunity, err := community.ToProtocolMessageBytes()
s.Require().NoError(err)
// Unwrap signer (Admin) data at user side.
signer, description, err := communities.UnwrapCommunityDescriptionMessage(wrappedCommunity)
s.Require().NoError(err)
// Handle community data state at receiver side
messageState := user.buildMessageState()
messageState.CurrentMessageState = &CurrentMessageState{}
messageState.CurrentMessageState.PublicKey = &user.identity.PublicKey
err = user.handleCommunityDescription(messageState, signer, description, wrappedCommunity, nil)
s.Require().NoError(err)
}
func joinCommunity(s *suite.Suite, community *communities.Community, owner *Messenger, user *Messenger, request *requests.RequestToJoinCommunity, password string) {
if password != "" {
signingParams, err := user.GenerateJoiningCommunityRequestsForSigning(common.PubkeyToHex(&user.identity.PublicKey), community.ID(), request.AddressesToReveal)
s.Require().NoError(err)
for i := range signingParams {
signingParams[i].Password = password
}
signatures, err := user.SignData(signingParams)
s.Require().NoError(err)
updateAddresses := len(request.AddressesToReveal) == 0
if updateAddresses {
request.AddressesToReveal = make([]string, len(signingParams))
}
for i := range signingParams {
request.AddressesToReveal[i] = signingParams[i].Address
request.Signatures = append(request.Signatures, types.FromHex(signatures[i]))
}
if updateAddresses {
request.AirdropAddress = request.AddressesToReveal[0]
}
}
response, err := user.RequestToJoinCommunity(request)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.RequestsToJoinCommunity, 1)
s.Require().Len(response.ActivityCenterNotifications(), 1)
notification := response.ActivityCenterNotifications()[0]
s.Require().NotNil(notification)
s.Require().Equal(notification.Type, ActivityCenterNotificationTypeCommunityRequest)
s.Require().Equal(notification.MembershipStatus, ActivityCenterMembershipStatusPending)
// Retrieve and accept join request
_, err = WaitOnMessengerResponse(owner, func(r *MessengerResponse) bool {
return len(r.Communities()) > 0 && r.Communities()[0].HasMember(&user.identity.PublicKey)
}, "user not accepted")
s.Require().NoError(err)
// Retrieve join request response
_, err = WaitOnMessengerResponse(user, func(r *MessengerResponse) bool {
return len(r.Communities()) > 0 && r.Communities()[0].HasMember(&user.identity.PublicKey)
}, "user not accepted")
s.Require().NoError(err)
}
func requestToJoinCommunity(s *suite.Suite, controlNode *Messenger, user *Messenger, request *requests.RequestToJoinCommunity) types.HexBytes {
response, err := user.RequestToJoinCommunity(request)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.RequestsToJoinCommunity, 1)
requestToJoin := response.RequestsToJoinCommunity[0]
s.Require().Equal(requestToJoin.PublicKey, common.PubkeyToHex(&user.identity.PublicKey))
_, err = WaitOnMessengerResponse(
controlNode,
func(r *MessengerResponse) bool {
if len(r.RequestsToJoinCommunity) == 0 {
return false
}
for _, resultRequest := range r.RequestsToJoinCommunity {
if resultRequest.PublicKey == common.PubkeyToHex(&user.identity.PublicKey) {
return true
}
}
return false
},
"control node did not receive community request to join",
)
s.Require().NoError(err)
return requestToJoin.ID
}
func joinOnRequestCommunity(s *suite.Suite, community *communities.Community, controlNode *Messenger, user *Messenger, request *requests.RequestToJoinCommunity) {
// Request to join the community
requestToJoinID := requestToJoinCommunity(s, controlNode, user, request)
// accept join request
acceptRequestToJoin := &requests.AcceptRequestToJoinCommunity{ID: requestToJoinID}
response, err := controlNode.AcceptRequestToJoinCommunity(acceptRequestToJoin)
s.Require().NoError(err)
s.Require().NotNil(response)
updatedCommunity := response.Communities()[0]
s.Require().NotNil(updatedCommunity)
s.Require().True(updatedCommunity.HasMember(&user.identity.PublicKey))
// receive request to join response
_, err = WaitOnMessengerResponse(
user,
func(r *MessengerResponse) bool {
return len(r.Communities()) > 0 && r.Communities()[0].HasMember(&user.identity.PublicKey)
},
"user did not receive request to join response",
)
s.Require().NoError(err)
userCommunity, err := user.GetCommunityByID(community.ID())
s.Require().NoError(err)
s.Require().True(userCommunity.HasMember(&user.identity.PublicKey))
_, err = WaitOnMessengerResponse(
controlNode,
func(r *MessengerResponse) bool {
return len(r.Communities()) > 0 && r.Communities()[0].HasMember(&user.identity.PublicKey)
},
"control node did not receive request to join response",
)
s.Require().NoError(err)
}
func sendChatMessage(s *suite.Suite, sender *Messenger, chatID string, text string) *common.Message {
msg := &common.Message{
ChatMessage: &protobuf.ChatMessage{
ChatId: chatID,
ContentType: protobuf.ChatMessage_TEXT_PLAIN,
Text: text,
},
}
_, err := sender.SendChatMessage(context.Background(), msg)
s.Require().NoError(err)
return msg
}
func grantPermission(s *suite.Suite, community *communities.Community, controlNode *Messenger, target *Messenger, role protobuf.CommunityMember_Roles) {
responseAddRole, err := controlNode.AddRoleToMember(&requests.AddRoleToMember{
CommunityID: community.ID(),
User: common.PubkeyToHexBytes(target.IdentityPublicKey()),
Role: role,
})
s.Require().NoError(err)
s.Require().NoError(checkRolePermissionInResponse(responseAddRole, target.IdentityPublicKey(), role))
response, err := WaitOnMessengerResponse(target, func(response *MessengerResponse) bool {
if len(response.Communities()) == 0 {
return false
}
err := checkRolePermissionInResponse(response, target.IdentityPublicKey(), role)
return err == nil
}, "community description changed message not received")
s.Require().NoError(err)
s.Require().NoError(checkRolePermissionInResponse(response, target.IdentityPublicKey(), role))
}
func checkRolePermissionInResponse(response *MessengerResponse, member *ecdsa.PublicKey, role protobuf.CommunityMember_Roles) error {
if len(response.Communities()) == 0 {
return errors.New("Response does not contain communities")
}
rCommunities := response.Communities()
switch role {
case protobuf.CommunityMember_ROLE_OWNER:
if !rCommunities[0].IsMemberOwner(member) {
return errors.New("Member without owner role")
}
case protobuf.CommunityMember_ROLE_ADMIN:
if !rCommunities[0].IsMemberAdmin(member) {
return errors.New("Member without admin role")
}
case protobuf.CommunityMember_ROLE_TOKEN_MASTER:
if !rCommunities[0].IsMemberTokenMaster(member) {
return errors.New("Member without token master role")
}
default:
return errors.New("Can't check unknonw member role")
}
return nil
}
func checkMemberJoinedToTheCommunity(response *MessengerResponse, member *ecdsa.PublicKey) error {
if len(response.Communities()) == 0 {
return errors.New("No communities in the response")
}
if !response.Communities()[0].HasMember(member) {
return errors.New("Member was not added to the community")
}
return nil
}
func waitOnCommunitiesEvent(user *Messenger, condition func(*communities.Subscription) bool) <-chan error {
errCh := make(chan error, 1)
go func() {
defer close(errCh)
for {
select {
case sub, more := <-user.communitiesManager.Subscribe():
if !more {
errCh <- errors.New("channel closed when waiting for communities event")
return
}
if condition(sub) {
return
}
case <-time.After(500 * time.Millisecond):
errCh <- errors.New("timed out when waiting for communities event")
return
}
}
}()
return errCh
}