package protocol import ( "context" "crypto/ecdsa" "encoding/json" "errors" "sync" "time" "github.com/stretchr/testify/suite" 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/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "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/communities/token" "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/requests" walletCommon "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/thirdparty" walletToken "github.com/status-im/status-go/services/wallet/token" "github.com/status-im/status-go/transactions" ) 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 *communities.BalancesByChain } 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) GetCachedBalancesByChain(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 CollectiblesManagerMock struct { Collectibles *communities.CollectiblesByChain collectibleOwnershipResponse map[string][]thirdparty.AccountBalance } func (m *CollectiblesManagerMock) FetchCachedBalancesByOwnerAndContractAddress(ctx context.Context, chainID walletCommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) { return m.FetchBalancesByOwnerAndContractAddress(ctx, chainID, ownerAddress, contractAddresses) } func (m *CollectiblesManagerMock) FetchBalancesByOwnerAndContractAddress(ctx context.Context, chainID walletCommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) { ret := make(thirdparty.TokenBalancesPerContractAddress) accountsBalances, ok := (*m.Collectibles)[uint64(chainID)] if !ok { return ret, nil } balances, ok := accountsBalances[ownerAddress] if !ok { return ret, nil } for _, contractAddress := range contractAddresses { balance, ok := balances[contractAddress] if ok { ret[contractAddress] = balance } } return ret, nil } func (m *CollectiblesManagerMock) GetCollectibleOwnership(requestedID thirdparty.CollectibleUniqueID) ([]thirdparty.AccountBalance, error) { for id, balances := range m.collectibleOwnershipResponse { if id == requestedID.HashKey() { return balances, nil } } return []thirdparty.AccountBalance{}, nil } func (m *CollectiblesManagerMock) FetchCollectibleOwnersByContractAddress(ctx context.Context, chainID walletCommon.ChainID, contractAddress gethcommon.Address) (*thirdparty.CollectibleContractOwnership, error) { ret := &thirdparty.CollectibleContractOwnership{ ContractAddress: contractAddress, Owners: []thirdparty.CollectibleOwner{}, } accountsBalances, ok := (*m.Collectibles)[uint64(chainID)] if !ok { return ret, nil } for wallet, balances := range accountsBalances { balance, ok := balances[contractAddress] if ok { ret.Owners = append(ret.Owners, thirdparty.CollectibleOwner{ OwnerAddress: wallet, TokenBalances: balance, }) } } return ret, nil } func (m *CollectiblesManagerMock) SetCollectibleOwnershipResponse(id thirdparty.CollectibleUniqueID, balances []thirdparty.AccountBalance) { if m.collectibleOwnershipResponse == nil { m.collectibleOwnershipResponse = map[string][]thirdparty.AccountBalance{} } m.collectibleOwnershipResponse[id.HashKey()] = balances } type CollectiblesServiceMock struct { Collectibles map[uint64]map[string]*communities.CollectibleContractData Assets map[uint64]map[string]*communities.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) (*communities.CollectibleContractData, error) { collectibleContractData, dataExists := c.Collectibles[chainID][contractAddress] if dataExists { return collectibleContractData, nil } return nil, nil } func (c *CollectiblesServiceMock) GetAssetContractData(chainID uint64, contractAddress string) (*communities.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 *communities.CollectibleContractData) { if c.Collectibles == nil { c.Collectibles = make(map[uint64]map[string]*communities.CollectibleContractData) } if _, ok := c.Collectibles[chainID]; !ok { c.Collectibles[chainID] = make(map[string]*communities.CollectibleContractData) } c.Collectibles[chainID][contractAddress] = collectible } func (c *CollectiblesServiceMock) SetMockCommunityTokenData(token *token.CommunityToken) { if c.Collectibles == nil { c.Collectibles = make(map[uint64]map[string]*communities.CollectibleContractData) } data := &communities.CollectibleContractData{ TotalSupply: token.Supply, Transferable: token.Transferable, RemoteBurnable: token.RemoteSelfDestruct, InfiniteSupply: token.InfiniteSupply, } c.SetMockCollectibleContractData(uint64(token.ChainID), token.Address, data) } 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 *communities.AssetContractData) { if c.Assets == nil { c.Assets = make(map[uint64]map[string]*communities.AssetContractData) } c.Assets[chainID] = make(map[string]*communities.AssetContractData) c.Assets[chainID][contractAddress] = assetData } func (c *CollectiblesServiceMock) DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error) { return gethcommon.Hex2Bytes("ccbb375343347491706cf4b43796f7b96ccc89c9e191a8b78679daeba1684ec7"), nil } func (s *CollectiblesServiceMock) ProcessCommunityTokenAction(message *protobuf.CommunityTokenAction) error { return nil } type testCommunitiesMessengerConfig struct { testMessengerConfig nodeConfig *params.NodeConfig appSettings *settings.Settings password string walletAddresses []string mockedBalances *communities.BalancesByChain mockedCollectibles *communities.CollectiblesByChain collectiblesService communities.CommunityTokensServiceInterface } func (tcmc *testCommunitiesMessengerConfig) complete() error { err := tcmc.testMessengerConfig.complete() if err != nil { return err } if tcmc.nodeConfig == nil { tcmc.nodeConfig = defaultTestCommunitiesMessengerNodeConfig() } if tcmc.appSettings == nil { tcmc.appSettings = defaultTestCommunitiesMessengerSettings() } return nil } func defaultTestCommunitiesMessengerNodeConfig() *params.NodeConfig { return ¶ms.NodeConfig{ NetworkID: 10, DataDir: "test", } } func defaultTestCommunitiesMessengerSettings() *settings.Settings { networks := json.RawMessage("{}") return &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")} } func newTestCommunitiesMessenger(s *suite.Suite, waku types.Waku, config testCommunitiesMessengerConfig) *Messenger { err := config.complete() s.Require().NoError(err) accountsManagerMock := &AccountManagerMock{} accountsManagerMock.AccountsMap = make(map[string]string) for _, walletAddress := range config.walletAddresses { accountsManagerMock.AccountsMap[walletAddress] = types.EncodeHex(crypto.Keccak256([]byte(config.password))) } tokenManagerMock := &TokenManagerMock{ Balances: config.mockedBalances, } collectiblesManagerMock := &CollectiblesManagerMock{ Collectibles: config.mockedCollectibles, } options := []Option{ WithAccountManager(accountsManagerMock), WithTokenManager(tokenManagerMock), WithCollectiblesManager(collectiblesManagerMock), WithCommunityTokensService(config.collectiblesService), WithAppSettings(*config.appSettings, *config.nodeConfig), } config.extraOptions = append(config.extraOptions, options...) messenger, err := newTestMessenger(waku, config.testMessengerConfig) 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 config.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(config.walletAddresses)) for i := range config.walletAddresses { s.Require().Equal(walletAccounts[i].Type, accounts.AccountTypeGenerated) } return messenger } func createEncryptedCommunity(s *suite.Suite, owner *Messenger) (*communities.Community, *Chat) { community, chat := createCommunityConfigurable(s, owner, protobuf.CommunityPermissions_AUTO_ACCEPT) // Add community permission _, err := owner.CreateCommunityTokenPermission(&requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, TokenCriteria: []*protobuf.TokenCriteria{{ ContractAddresses: map[uint64]string{3: "0x933"}, Type: protobuf.CommunityTokenType_ERC20, Symbol: "STT", Name: "Status Test Token", AmountInWei: "10000000000000000000", Decimals: 18, }}, }) s.Require().NoError(err) // Add channel permission response, err := owner.CreateCommunityTokenPermission(&requests.CreateCommunityTokenPermission{ CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL, TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ ContractAddresses: map[uint64]string{3: "0x933"}, Type: protobuf.CommunityTokenType_ERC20, Symbol: "STT", Name: "Status Test Token", AmountInWei: "10000000000000000000", Decimals: 18, }, }, ChatIds: []string{chat.ID}, }) s.Require().NoError(err) s.Require().Len(response.Communities(), 1) community = response.Communities()[0] s.Require().True(community.Encrypted()) s.Require().True(community.ChannelEncrypted(chat.CommunityChatID())) return community, chat } 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(community.Chats(), 1) s.Require().Len(response.CommunitiesSettings(), 1) communitySettings := response.CommunitiesSettings()[0] s.Require().Equal(communitySettings.CommunityID, community.IDString()) s.Require().Equal(communitySettings.HistoryArchiveSupportEnabled, false) 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 // TODO: handle shards? err = user.handleCommunityDescription(messageState, signer, description, wrappedCommunity, nil, 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) subscription := user.communitiesManager.Subscribe() for { select { case sub, more := <-subscription: if !more { errCh <- errors.New("channel closed when waiting for communities event") return } if condition(sub) { return } case <-time.After(5 * time.Second): errCh <- errors.New("timed out when waiting for communities event") return } } }() return errCh }