github.com/status-im/status-go@v1.1.0/services/ext/service.go (about)

     1  package ext
     2  
     3  import (
     4  	"context"
     5  	"crypto/ecdsa"
     6  	"database/sql"
     7  	"encoding/hex"
     8  	"errors"
     9  	"fmt"
    10  	"math/big"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/syndtr/goleveldb/leveldb"
    17  	"go.uber.org/zap"
    18  
    19  	commongethtypes "github.com/ethereum/go-ethereum/common"
    20  	gethtypes "github.com/ethereum/go-ethereum/core/types"
    21  	"github.com/ethereum/go-ethereum/ethclient"
    22  	"github.com/ethereum/go-ethereum/event"
    23  	"github.com/ethereum/go-ethereum/log"
    24  	"github.com/ethereum/go-ethereum/node"
    25  	"github.com/ethereum/go-ethereum/p2p"
    26  	"github.com/ethereum/go-ethereum/p2p/enode"
    27  	gethrpc "github.com/ethereum/go-ethereum/rpc"
    28  
    29  	"github.com/status-im/status-go/account"
    30  	"github.com/status-im/status-go/api/multiformat"
    31  	"github.com/status-im/status-go/connection"
    32  	"github.com/status-im/status-go/db"
    33  	coretypes "github.com/status-im/status-go/eth-node/core/types"
    34  	"github.com/status-im/status-go/eth-node/crypto"
    35  	"github.com/status-im/status-go/eth-node/types"
    36  	"github.com/status-im/status-go/images"
    37  	"github.com/status-im/status-go/multiaccounts"
    38  	"github.com/status-im/status-go/multiaccounts/accounts"
    39  	"github.com/status-im/status-go/params"
    40  	"github.com/status-im/status-go/protocol"
    41  	"github.com/status-im/status-go/protocol/anonmetrics"
    42  	"github.com/status-im/status-go/protocol/common"
    43  	"github.com/status-im/status-go/protocol/communities"
    44  	"github.com/status-im/status-go/protocol/communities/token"
    45  	"github.com/status-im/status-go/protocol/protobuf"
    46  	"github.com/status-im/status-go/protocol/pushnotificationclient"
    47  	"github.com/status-im/status-go/protocol/pushnotificationserver"
    48  	"github.com/status-im/status-go/protocol/transport"
    49  	"github.com/status-im/status-go/rpc"
    50  	"github.com/status-im/status-go/server"
    51  	"github.com/status-im/status-go/services/browsers"
    52  	"github.com/status-im/status-go/services/communitytokens"
    53  	"github.com/status-im/status-go/services/ext/mailservers"
    54  	mailserversDB "github.com/status-im/status-go/services/mailservers"
    55  	"github.com/status-im/status-go/services/wallet"
    56  	"github.com/status-im/status-go/services/wallet/collectibles"
    57  	w_common "github.com/status-im/status-go/services/wallet/common"
    58  	"github.com/status-im/status-go/services/wallet/thirdparty"
    59  	"github.com/status-im/status-go/wakuv2"
    60  )
    61  
    62  const infinityString = "∞"
    63  const providerID = "community"
    64  
    65  // EnvelopeEventsHandler used for two different event types.
    66  type EnvelopeEventsHandler interface {
    67  	EnvelopeSent([][]byte)
    68  	EnvelopeExpired([][]byte, error)
    69  	MailServerRequestCompleted(types.Hash, types.Hash, []byte, error)
    70  	MailServerRequestExpired(types.Hash)
    71  }
    72  
    73  // Service is a service that provides some additional API to whisper-based protocols like Whisper or Waku.
    74  type Service struct {
    75  	messenger       *protocol.Messenger
    76  	identity        *ecdsa.PrivateKey
    77  	cancelMessenger chan struct{}
    78  	storage         db.TransactionalStorage
    79  	n               types.Node
    80  	rpcClient       *rpc.Client
    81  	config          params.NodeConfig
    82  	mailMonitor     *MailRequestMonitor
    83  	server          *p2p.Server
    84  	peerStore       *mailservers.PeerStore
    85  	accountsDB      *accounts.Database
    86  	multiAccountsDB *multiaccounts.Database
    87  	account         *multiaccounts.Account
    88  }
    89  
    90  // Make sure that Service implements node.Service interface.
    91  var _ node.Lifecycle = (*Service)(nil)
    92  
    93  func New(
    94  	config params.NodeConfig,
    95  	n types.Node,
    96  	rpcClient *rpc.Client,
    97  	ldb *leveldb.DB,
    98  	mailMonitor *MailRequestMonitor,
    99  	eventSub mailservers.EnvelopeEventSubscriber,
   100  ) *Service {
   101  	cache := mailservers.NewCache(ldb)
   102  	peerStore := mailservers.NewPeerStore(cache)
   103  	return &Service{
   104  		storage:     db.NewLevelDBStorage(ldb),
   105  		n:           n,
   106  		rpcClient:   rpcClient,
   107  		config:      config,
   108  		mailMonitor: mailMonitor,
   109  		peerStore:   peerStore,
   110  	}
   111  }
   112  
   113  func (s *Service) NodeID() *ecdsa.PrivateKey {
   114  	if s.server == nil {
   115  		return nil
   116  	}
   117  	return s.server.PrivateKey
   118  }
   119  
   120  func (s *Service) GetPeer(rawURL string) (*enode.Node, error) {
   121  	if len(rawURL) == 0 {
   122  		return mailservers.GetFirstConnected(s.server, s.peerStore)
   123  	}
   124  	return enode.ParseV4(rawURL)
   125  }
   126  
   127  func (s *Service) InitProtocol(nodeName string, identity *ecdsa.PrivateKey, appDb, walletDb *sql.DB, httpServer *server.MediaServer, multiAccountDb *multiaccounts.Database, acc *multiaccounts.Account, accountManager *account.GethManager, rpcClient *rpc.Client, walletService *wallet.Service, communityTokensService *communitytokens.Service, wakuService *wakuv2.Waku, logger *zap.Logger, accountsFeed *event.Feed) error {
   128  	var err error
   129  	if !s.config.ShhextConfig.PFSEnabled {
   130  		return nil
   131  	}
   132  
   133  	// If Messenger has been already set up, we need to shut it down
   134  	// before we init it again. Otherwise, it will lead to goroutines leakage
   135  	// due to not stopped filters.
   136  	if s.messenger != nil {
   137  		if err := s.messenger.Shutdown(); err != nil {
   138  			return err
   139  		}
   140  	}
   141  
   142  	s.identity = identity
   143  
   144  	// This directory should have already been created in loadNodeConfig, keeping this to ensure.
   145  	dataDir := filepath.Clean(s.config.RootDataDir)
   146  
   147  	if err := os.MkdirAll(dataDir, os.ModePerm); err != nil {
   148  		return err
   149  	}
   150  
   151  	envelopesMonitorConfig := &transport.EnvelopesMonitorConfig{
   152  		MaxAttempts:                      s.config.ShhextConfig.MaxMessageDeliveryAttempts,
   153  		AwaitOnlyMailServerConfirmations: s.config.ShhextConfig.MailServerConfirmations,
   154  		IsMailserver: func(peer types.EnodeID) bool {
   155  			return s.peerStore.Exist(peer)
   156  		},
   157  		EnvelopeEventsHandler: EnvelopeSignalHandler{},
   158  		Logger:                logger,
   159  	}
   160  	s.accountsDB, err = accounts.NewDB(appDb)
   161  	if err != nil {
   162  		return err
   163  	}
   164  	s.multiAccountsDB = multiAccountDb
   165  	s.account = acc
   166  
   167  	options, err := buildMessengerOptions(s.config, identity, appDb, walletDb, httpServer, s.rpcClient, s.multiAccountsDB, acc, envelopesMonitorConfig, s.accountsDB, walletService, communityTokensService, wakuService, logger, &MessengerSignalsHandler{}, accountManager, accountsFeed)
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	messenger, err := protocol.NewMessenger(
   173  		nodeName,
   174  		identity,
   175  		s.n,
   176  		s.config.ShhextConfig.InstallationID,
   177  		s.peerStore,
   178  		params.Version,
   179  		options...,
   180  	)
   181  	if err != nil {
   182  		return err
   183  	}
   184  	s.messenger = messenger
   185  	s.messenger.SetP2PServer(s.server)
   186  	if s.config.ProcessBackedupMessages {
   187  		s.messenger.EnableBackedupMessagesProcessing()
   188  	}
   189  
   190  	// Be mindful of adding more initialization code, as it can easily
   191  	// impact login times for mobile users. For example, we avoid calling
   192  	// messenger.InitFilters here.
   193  	return s.messenger.InitInstallations()
   194  }
   195  
   196  func (s *Service) StartMessenger() (*protocol.MessengerResponse, error) {
   197  	// Start a loop that retrieves all messages and propagates them to status-mobile.
   198  	s.cancelMessenger = make(chan struct{})
   199  	response, err := s.messenger.Start()
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	s.messenger.StartRetrieveMessagesLoop(time.Second, s.cancelMessenger)
   204  	go s.verifyTransactionLoop(30*time.Second, s.cancelMessenger)
   205  
   206  	if s.config.ShhextConfig.BandwidthStatsEnabled {
   207  		go s.retrieveStats(5*time.Second, s.cancelMessenger)
   208  	}
   209  
   210  	return response, nil
   211  }
   212  
   213  func (s *Service) retrieveStats(tick time.Duration, cancel <-chan struct{}) {
   214  	ticker := time.NewTicker(tick)
   215  	defer ticker.Stop()
   216  
   217  	for {
   218  		select {
   219  		case <-ticker.C:
   220  			response := s.messenger.GetStats()
   221  			PublisherSignalHandler{}.Stats(response)
   222  		case <-cancel:
   223  			return
   224  		}
   225  	}
   226  }
   227  
   228  type verifyTransactionClient struct {
   229  	chainID *big.Int
   230  	url     string
   231  }
   232  
   233  func (c *verifyTransactionClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, coretypes.TransactionStatus, error) {
   234  	signer := gethtypes.NewLondonSigner(c.chainID)
   235  	client, err := ethclient.Dial(c.url)
   236  	if err != nil {
   237  		return coretypes.Message{}, coretypes.TransactionStatusPending, err
   238  	}
   239  
   240  	transaction, pending, err := client.TransactionByHash(ctx, commongethtypes.BytesToHash(hash.Bytes()))
   241  	if err != nil {
   242  		return coretypes.Message{}, coretypes.TransactionStatusPending, err
   243  	}
   244  
   245  	message, err := transaction.AsMessage(signer, nil)
   246  	if err != nil {
   247  		return coretypes.Message{}, coretypes.TransactionStatusPending, err
   248  	}
   249  	from := types.BytesToAddress(message.From().Bytes())
   250  	to := types.BytesToAddress(message.To().Bytes())
   251  
   252  	if pending {
   253  		return coretypes.NewMessage(
   254  			from,
   255  			&to,
   256  			message.Nonce(),
   257  			message.Value(),
   258  			message.Gas(),
   259  			message.GasPrice(),
   260  			message.Data(),
   261  			message.CheckNonce(),
   262  		), coretypes.TransactionStatusPending, nil
   263  	}
   264  
   265  	receipt, err := client.TransactionReceipt(ctx, commongethtypes.BytesToHash(hash.Bytes()))
   266  	if err != nil {
   267  		return coretypes.Message{}, coretypes.TransactionStatusPending, err
   268  	}
   269  
   270  	coremessage := coretypes.NewMessage(
   271  		from,
   272  		&to,
   273  		message.Nonce(),
   274  		message.Value(),
   275  		message.Gas(),
   276  		message.GasPrice(),
   277  		message.Data(),
   278  		message.CheckNonce(),
   279  	)
   280  
   281  	// Token transfer, check the logs
   282  	if len(coremessage.Data()) != 0 {
   283  		if w_common.IsTokenTransfer(receipt.Logs) {
   284  			return coremessage, coretypes.TransactionStatus(receipt.Status), nil
   285  		}
   286  		return coremessage, coretypes.TransactionStatusFailed, nil
   287  	}
   288  
   289  	return coremessage, coretypes.TransactionStatus(receipt.Status), nil
   290  }
   291  
   292  func (s *Service) verifyTransactionLoop(tick time.Duration, cancel <-chan struct{}) {
   293  	if s.config.ShhextConfig.VerifyTransactionURL == "" {
   294  		log.Warn("not starting transaction loop")
   295  		return
   296  	}
   297  
   298  	ticker := time.NewTicker(tick)
   299  	defer ticker.Stop()
   300  
   301  	ctx, cancelVerifyTransaction := context.WithCancel(context.Background())
   302  
   303  	for {
   304  		select {
   305  		case <-ticker.C:
   306  			accounts, err := s.accountsDB.GetActiveAccounts()
   307  			if err != nil {
   308  				log.Error("failed to retrieve accounts", "err", err)
   309  			}
   310  			var wallets []types.Address
   311  			for _, account := range accounts {
   312  				if account.IsWalletNonWatchOnlyAccount() {
   313  					wallets = append(wallets, types.BytesToAddress(account.Address.Bytes()))
   314  				}
   315  			}
   316  
   317  			response, err := s.messenger.ValidateTransactions(ctx, wallets)
   318  			if err != nil {
   319  				log.Error("failed to validate transactions", "err", err)
   320  				continue
   321  			}
   322  			s.messenger.PublishMessengerResponse(response)
   323  
   324  		case <-cancel:
   325  			cancelVerifyTransaction()
   326  			return
   327  		}
   328  	}
   329  }
   330  
   331  func (s *Service) EnableInstallation(installationID string) error {
   332  	return s.messenger.EnableInstallation(installationID)
   333  }
   334  
   335  // DisableInstallation disables an installation for multi-device sync.
   336  func (s *Service) DisableInstallation(installationID string) error {
   337  	return s.messenger.DisableInstallation(installationID)
   338  }
   339  
   340  // Protocols returns a new protocols list. In this case, there are none.
   341  func (s *Service) Protocols() []p2p.Protocol {
   342  	return []p2p.Protocol{}
   343  }
   344  
   345  // APIs returns a list of new APIs.
   346  func (s *Service) APIs() []gethrpc.API {
   347  	panic("this is abstract service, use shhext or wakuext implementation")
   348  }
   349  
   350  func (s *Service) SetP2PServer(server *p2p.Server) {
   351  	s.server = server
   352  }
   353  
   354  // Start is run when a service is started.
   355  // It does nothing in this case but is required by `node.Service` interface.
   356  func (s *Service) Start() error {
   357  	return nil
   358  }
   359  
   360  // Stop is run when a service is stopped.
   361  func (s *Service) Stop() error {
   362  	log.Info("Stopping shhext service")
   363  	if s.cancelMessenger != nil {
   364  		select {
   365  		case <-s.cancelMessenger:
   366  			// channel already closed
   367  		default:
   368  			close(s.cancelMessenger)
   369  			s.cancelMessenger = nil
   370  		}
   371  	}
   372  
   373  	if s.messenger != nil {
   374  		if err := s.messenger.Shutdown(); err != nil {
   375  			log.Error("failed to stop messenger", "err", err)
   376  			return err
   377  		}
   378  		s.messenger = nil
   379  	}
   380  
   381  	return nil
   382  }
   383  
   384  func buildMessengerOptions(
   385  	config params.NodeConfig,
   386  	identity *ecdsa.PrivateKey,
   387  	appDb *sql.DB,
   388  	walletDb *sql.DB,
   389  	httpServer *server.MediaServer,
   390  	rpcClient *rpc.Client,
   391  	multiAccounts *multiaccounts.Database,
   392  	account *multiaccounts.Account,
   393  	envelopesMonitorConfig *transport.EnvelopesMonitorConfig,
   394  	accountsDB *accounts.Database,
   395  	walletService *wallet.Service,
   396  	communityTokensService *communitytokens.Service,
   397  	wakuService *wakuv2.Waku,
   398  	logger *zap.Logger,
   399  	messengerSignalsHandler protocol.MessengerSignalsHandler,
   400  	accountManager account.Manager,
   401  	accountsFeed *event.Feed,
   402  ) ([]protocol.Option, error) {
   403  	options := []protocol.Option{
   404  		protocol.WithCustomLogger(logger),
   405  		protocol.WithPushNotifications(),
   406  		protocol.WithDatabase(appDb),
   407  		protocol.WithWalletDatabase(walletDb),
   408  		protocol.WithMultiAccounts(multiAccounts),
   409  		protocol.WithMailserversDatabase(mailserversDB.NewDB(appDb)),
   410  		protocol.WithAccount(account),
   411  		protocol.WithBrowserDatabase(browsers.NewDB(appDb)),
   412  		protocol.WithEnvelopesMonitorConfig(envelopesMonitorConfig),
   413  		protocol.WithSignalsHandler(messengerSignalsHandler),
   414  		protocol.WithENSVerificationConfig(config.ShhextConfig.VerifyENSURL, config.ShhextConfig.VerifyENSContractAddress),
   415  		protocol.WithClusterConfig(config.ClusterConfig),
   416  		protocol.WithTorrentConfig(&config.TorrentConfig),
   417  		protocol.WithHTTPServer(httpServer),
   418  		protocol.WithRPCClient(rpcClient),
   419  		protocol.WithMessageCSV(config.OutputMessageCSVEnabled),
   420  		protocol.WithWalletConfig(&config.WalletConfig),
   421  		protocol.WithWalletService(walletService),
   422  		protocol.WithCommunityTokensService(communityTokensService),
   423  		protocol.WithWakuService(wakuService),
   424  		protocol.WithAccountManager(accountManager),
   425  		protocol.WithAccountsFeed(accountsFeed),
   426  	}
   427  
   428  	if config.ShhextConfig.DataSyncEnabled {
   429  		options = append(options, protocol.WithDatasync())
   430  	}
   431  
   432  	settings, err := accountsDB.GetSettings()
   433  	if err != sql.ErrNoRows && err != nil {
   434  		return nil, err
   435  	}
   436  
   437  	// Generate anon metrics client config
   438  	if settings.AnonMetricsShouldSend {
   439  		keyBytes, err := hex.DecodeString(config.ShhextConfig.AnonMetricsSendID)
   440  		if err != nil {
   441  			return nil, err
   442  		}
   443  
   444  		key, err := crypto.UnmarshalPubkey(keyBytes)
   445  		if err != nil {
   446  			return nil, err
   447  		}
   448  
   449  		amcc := &anonmetrics.ClientConfig{
   450  			ShouldSend:  true,
   451  			SendAddress: key,
   452  		}
   453  		options = append(options, protocol.WithAnonMetricsClientConfig(amcc))
   454  	}
   455  
   456  	// Generate anon metrics server config
   457  	if config.ShhextConfig.AnonMetricsServerEnabled {
   458  		if len(config.ShhextConfig.AnonMetricsServerPostgresURI) == 0 {
   459  			return nil, errors.New("AnonMetricsServerPostgresURI must be set")
   460  		}
   461  
   462  		amsc := &anonmetrics.ServerConfig{
   463  			Enabled:     true,
   464  			PostgresURI: config.ShhextConfig.AnonMetricsServerPostgresURI,
   465  		}
   466  		options = append(options, protocol.WithAnonMetricsServerConfig(amsc))
   467  	}
   468  
   469  	if settings.TelemetryServerURL != "" {
   470  		options = append(options, protocol.WithTelemetry(settings.TelemetryServerURL, time.Duration(settings.TelemetrySendPeriodMs)*time.Millisecond))
   471  	}
   472  
   473  	if settings.PushNotificationsServerEnabled {
   474  		config := &pushnotificationserver.Config{
   475  			Enabled: true,
   476  			Logger:  logger,
   477  		}
   478  		options = append(options, protocol.WithPushNotificationServerConfig(config))
   479  	}
   480  
   481  	var pushNotifServKey []*ecdsa.PublicKey
   482  	for _, d := range config.ShhextConfig.DefaultPushNotificationsServers {
   483  		pushNotifServKey = append(pushNotifServKey, d.PublicKey)
   484  	}
   485  
   486  	options = append(options, protocol.WithPushNotificationClientConfig(&pushnotificationclient.Config{
   487  		DefaultServers:             pushNotifServKey,
   488  		BlockMentions:              settings.PushNotificationsBlockMentions,
   489  		SendEnabled:                settings.SendPushNotifications,
   490  		AllowFromContactsOnly:      settings.PushNotificationsFromContactsOnly,
   491  		RemoteNotificationsEnabled: settings.RemotePushNotificationsEnabled,
   492  	}))
   493  
   494  	if config.ShhextConfig.VerifyTransactionURL != "" {
   495  		client := &verifyTransactionClient{
   496  			url:     config.ShhextConfig.VerifyTransactionURL,
   497  			chainID: big.NewInt(config.ShhextConfig.VerifyTransactionChainID),
   498  		}
   499  		options = append(options, protocol.WithVerifyTransactionClient(client))
   500  	}
   501  
   502  	return options, nil
   503  }
   504  
   505  func (s *Service) ConnectionChanged(state connection.State) {
   506  	if s.messenger != nil {
   507  		s.messenger.ConnectionChanged(state)
   508  	}
   509  }
   510  
   511  func (s *Service) Messenger() *protocol.Messenger {
   512  	return s.messenger
   513  }
   514  
   515  func tokenURIToCommunityID(tokenURI string) string {
   516  	tmpStr := strings.Split(tokenURI, "/")
   517  
   518  	// Community NFTs have a tokenURI of the form "compressedCommunityID/tokenID"
   519  	if len(tmpStr) != 2 {
   520  		return ""
   521  	}
   522  	compressedCommunityID := tmpStr[0]
   523  
   524  	hexCommunityID, err := multiformat.DeserializeCompressedKey(compressedCommunityID)
   525  	if err != nil {
   526  		return ""
   527  	}
   528  
   529  	pubKey, err := common.HexToPubkey(hexCommunityID)
   530  	if err != nil {
   531  		return ""
   532  	}
   533  
   534  	communityID := types.EncodeHex(crypto.CompressPubkey(pubKey))
   535  
   536  	return communityID
   537  }
   538  
   539  func (s *Service) GetCommunityID(tokenURI string) string {
   540  	if tokenURI != "" {
   541  		return tokenURIToCommunityID(tokenURI)
   542  	}
   543  	return ""
   544  }
   545  
   546  func (s *Service) FillCollectiblesMetadata(communityID string, cs []*thirdparty.FullCollectibleData) (bool, error) {
   547  	if s.messenger == nil {
   548  		return false, fmt.Errorf("messenger not ready")
   549  	}
   550  
   551  	community, err := s.fetchCommunityInfoForCollectibles(communityID, collectibles.IDsFromAssets(cs))
   552  	if err != nil {
   553  		return false, err
   554  	}
   555  	if community == nil {
   556  		return false, nil
   557  	}
   558  
   559  	for _, collectible := range cs {
   560  		err := s.FillCollectibleMetadata(community, collectible)
   561  		if err != nil {
   562  			return true, err
   563  		}
   564  	}
   565  
   566  	return true, nil
   567  }
   568  
   569  func (s *Service) FillCollectibleMetadata(community *communities.Community, collectible *thirdparty.FullCollectibleData) error {
   570  	if s.messenger == nil {
   571  		return fmt.Errorf("messenger not ready")
   572  	}
   573  
   574  	if collectible == nil {
   575  		return fmt.Errorf("empty collectible")
   576  	}
   577  
   578  	id := collectible.CollectibleData.ID
   579  	communityID := collectible.CollectibleData.CommunityID
   580  
   581  	if communityID == "" {
   582  		return fmt.Errorf("invalid communityID")
   583  	}
   584  
   585  	tokenMetadata, err := s.fetchCommunityCollectibleMetadata(community, id.ContractID)
   586  
   587  	if err != nil {
   588  		return err
   589  	}
   590  
   591  	if tokenMetadata == nil {
   592  		return nil
   593  	}
   594  
   595  	communityToken, err := s.fetchCommunityToken(communityID, id.ContractID)
   596  	if err != nil {
   597  		return err
   598  	}
   599  
   600  	permission := fetchCommunityCollectiblePermission(community, id)
   601  
   602  	privilegesLevel := token.CommunityLevel
   603  	if permission != nil {
   604  		privilegesLevel = permissionTypeToPrivilegesLevel(permission.GetType())
   605  	}
   606  
   607  	imagePayload, _ := images.GetPayloadFromURI(tokenMetadata.GetImage())
   608  
   609  	collectible.CollectibleData.ContractType = w_common.ContractTypeERC721
   610  	collectible.CollectibleData.Provider = providerID
   611  	collectible.CollectibleData.Name = tokenMetadata.GetName()
   612  	collectible.CollectibleData.Description = tokenMetadata.GetDescription()
   613  	collectible.CollectibleData.ImagePayload = imagePayload
   614  	collectible.CollectibleData.Traits = getCollectibleCommunityTraits(communityToken)
   615  	collectible.CollectibleData.Soulbound = !communityToken.Transferable
   616  
   617  	if collectible.CollectionData == nil {
   618  		collectible.CollectionData = &thirdparty.CollectionData{
   619  			ID:          id.ContractID,
   620  			CommunityID: communityID,
   621  		}
   622  	}
   623  	collectible.CollectionData.ContractType = w_common.ContractTypeERC721
   624  	collectible.CollectionData.Provider = providerID
   625  	collectible.CollectionData.Name = tokenMetadata.GetName()
   626  	collectible.CollectionData.ImagePayload = imagePayload
   627  
   628  	collectible.CommunityInfo = communityToInfo(community)
   629  
   630  	collectible.CollectibleCommunityInfo = &thirdparty.CollectibleCommunityInfo{
   631  		PrivilegesLevel: privilegesLevel,
   632  	}
   633  
   634  	return nil
   635  }
   636  
   637  func permissionTypeToPrivilegesLevel(permissionType protobuf.CommunityTokenPermission_Type) token.PrivilegesLevel {
   638  	switch permissionType {
   639  	case protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER:
   640  		return token.OwnerLevel
   641  	case protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER:
   642  		return token.MasterLevel
   643  	default:
   644  		return token.CommunityLevel
   645  	}
   646  }
   647  
   648  func communityToInfo(community *communities.Community) *thirdparty.CommunityInfo {
   649  	if community == nil {
   650  		return nil
   651  	}
   652  
   653  	return &thirdparty.CommunityInfo{
   654  		CommunityName:         community.Name(),
   655  		CommunityColor:        community.Color(),
   656  		CommunityImagePayload: fetchCommunityImage(community),
   657  	}
   658  }
   659  
   660  func (s *Service) fetchCommunityFromStoreNodes(communityID string) (*communities.Community, error) {
   661  	community, err := s.messenger.FetchCommunity(&protocol.FetchCommunityRequest{
   662  		CommunityKey:    communityID,
   663  		TryDatabase:     false,
   664  		WaitForResponse: true,
   665  	})
   666  	if err != nil {
   667  		return nil, err
   668  	}
   669  	return community, nil
   670  }
   671  
   672  // Fetch latest community from store nodes.
   673  func (s *Service) FetchCommunityInfo(communityID string) (*thirdparty.CommunityInfo, error) {
   674  	if s.messenger == nil {
   675  		return nil, fmt.Errorf("messenger not ready")
   676  	}
   677  
   678  	community, err := s.messenger.FindCommunityInfoFromDB(communityID)
   679  	if err != nil && err != communities.ErrOrgNotFound {
   680  		return nil, err
   681  	}
   682  
   683  	// Fetch latest version from store nodes
   684  	if community == nil || !community.IsControlNode() {
   685  		community, err = s.fetchCommunityFromStoreNodes(communityID)
   686  		if err != nil {
   687  			return nil, err
   688  		}
   689  	}
   690  
   691  	return communityToInfo(community), nil
   692  }
   693  
   694  // Fetch latest community from store nodes only if any collectibles data is missing.
   695  func (s *Service) fetchCommunityInfoForCollectibles(communityID string, ids []thirdparty.CollectibleUniqueID) (*communities.Community, error) {
   696  	community, err := s.messenger.FindCommunityInfoFromDB(communityID)
   697  	if err != nil && err != communities.ErrOrgNotFound {
   698  		return nil, err
   699  	}
   700  
   701  	if community == nil {
   702  		return s.fetchCommunityFromStoreNodes(communityID)
   703  	}
   704  
   705  	if community.IsControlNode() {
   706  		return community, nil
   707  	}
   708  
   709  	contractIDs := func() map[string]thirdparty.ContractID {
   710  		result := map[string]thirdparty.ContractID{}
   711  		for _, id := range ids {
   712  			result[id.HashKey()] = id.ContractID
   713  		}
   714  		return result
   715  	}()
   716  
   717  	hasAllMetadata := true
   718  	for _, contractID := range contractIDs {
   719  		tokenMetadata, err := s.fetchCommunityCollectibleMetadata(community, contractID)
   720  		if err != nil {
   721  			return nil, err
   722  		}
   723  		if tokenMetadata == nil {
   724  			hasAllMetadata = false
   725  			break
   726  		}
   727  	}
   728  
   729  	if !hasAllMetadata {
   730  		return s.fetchCommunityFromStoreNodes(communityID)
   731  	}
   732  
   733  	return community, nil
   734  }
   735  
   736  func (s *Service) fetchCommunityToken(communityID string, contractID thirdparty.ContractID) (*token.CommunityToken, error) {
   737  	if s.messenger == nil {
   738  		return nil, fmt.Errorf("messenger not ready")
   739  	}
   740  
   741  	return s.messenger.GetCommunityToken(communityID, int(contractID.ChainID), contractID.Address.String())
   742  }
   743  
   744  func (s *Service) fetchCommunityCollectibleMetadata(community *communities.Community, contractID thirdparty.ContractID) (*protobuf.CommunityTokenMetadata, error) {
   745  	tokensMetadata := community.CommunityTokensMetadata()
   746  
   747  	for _, tokenMetadata := range tokensMetadata {
   748  		contractAddresses := tokenMetadata.GetContractAddresses()
   749  		if contractAddresses[uint64(contractID.ChainID)] == contractID.Address.Hex() {
   750  			return tokenMetadata, nil
   751  		}
   752  	}
   753  
   754  	return nil, nil
   755  }
   756  
   757  func tokenCriterionContainsCollectible(tokenCriterion *protobuf.TokenCriteria, id thirdparty.CollectibleUniqueID) bool {
   758  	// Check if token type matches
   759  	if tokenCriterion.Type != protobuf.CommunityTokenType_ERC721 {
   760  		return false
   761  	}
   762  
   763  	for chainID, contractAddressStr := range tokenCriterion.ContractAddresses {
   764  		if chainID != uint64(id.ContractID.ChainID) {
   765  			continue
   766  		}
   767  
   768  		contractAddress := commongethtypes.HexToAddress(contractAddressStr)
   769  		if contractAddress != id.ContractID.Address {
   770  			continue
   771  		}
   772  
   773  		if len(tokenCriterion.TokenIds) == 0 {
   774  			return true
   775  		}
   776  
   777  		for _, tokenID := range tokenCriterion.TokenIds {
   778  			tokenIDBigInt := new(big.Int).SetUint64(tokenID)
   779  			if id.TokenID.Cmp(tokenIDBigInt) == 0 {
   780  				return true
   781  			}
   782  		}
   783  	}
   784  
   785  	return false
   786  }
   787  
   788  func permissionContainsCollectible(permission *communities.CommunityTokenPermission, id thirdparty.CollectibleUniqueID) bool {
   789  	// See if any token criterion contains the collectible we're looking for
   790  	for _, tokenCriterion := range permission.TokenCriteria {
   791  		if tokenCriterionContainsCollectible(tokenCriterion, id) {
   792  			return true
   793  		}
   794  	}
   795  	return false
   796  }
   797  
   798  func fetchCommunityCollectiblePermission(community *communities.Community, id thirdparty.CollectibleUniqueID) *communities.CommunityTokenPermission {
   799  	// Permnission types of interest
   800  	permissionTypes := []protobuf.CommunityTokenPermission_Type{
   801  		protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER,
   802  		protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER,
   803  	}
   804  
   805  	for _, permissionType := range permissionTypes {
   806  		permissions := community.TokenPermissionsByType(permissionType)
   807  		// See if any community permission matches the type we're looking for
   808  		for _, permission := range permissions {
   809  			if permissionContainsCollectible(permission, id) {
   810  				return permission
   811  			}
   812  		}
   813  	}
   814  
   815  	return nil
   816  }
   817  
   818  func fetchCommunityImage(community *communities.Community) []byte {
   819  	imageTypes := []string{
   820  		images.LargeDimName,
   821  		images.SmallDimName,
   822  	}
   823  
   824  	communityImages := community.Images()
   825  
   826  	for _, imageType := range imageTypes {
   827  		if pbImage, ok := communityImages[imageType]; ok {
   828  			return pbImage.Payload
   829  		}
   830  	}
   831  
   832  	return nil
   833  }
   834  
   835  func boolToString(value bool) string {
   836  	if value {
   837  		return "Yes"
   838  	}
   839  	return "No"
   840  }
   841  
   842  func getCollectibleCommunityTraits(token *token.CommunityToken) []thirdparty.CollectibleTrait {
   843  	if token == nil {
   844  		return make([]thirdparty.CollectibleTrait, 0)
   845  	}
   846  
   847  	totalStr := infinityString
   848  	availableStr := infinityString
   849  	if !token.InfiniteSupply {
   850  		totalStr = token.Supply.String()
   851  		// TODO: calculate available supply. See services/communitytokens/api.go
   852  		availableStr = totalStr
   853  	}
   854  
   855  	transferableStr := boolToString(token.Transferable)
   856  
   857  	destructibleStr := boolToString(token.RemoteSelfDestruct)
   858  
   859  	return []thirdparty.CollectibleTrait{
   860  		{
   861  			TraitType: "Symbol",
   862  			Value:     token.Symbol,
   863  		},
   864  		{
   865  			TraitType: "Total",
   866  			Value:     totalStr,
   867  		},
   868  		{
   869  			TraitType: "Available",
   870  			Value:     availableStr,
   871  		},
   872  		{
   873  			TraitType: "Transferable",
   874  			Value:     transferableStr,
   875  		},
   876  		{
   877  			TraitType: "Destructible",
   878  			Value:     destructibleStr,
   879  		},
   880  	}
   881  }