status-go/services/ext/service.go
Icaro Motta dbed69d155
perf(login)!: Set-up messenger filters outside login flow (#5229)
Fixes the slow login in mobile devices when users have joined large communities,
such as the Status one. A user would get stuck for almost 20s in some devices.

We identified that the step to set-up filters in the messenger is potentially
expensive and that it is not critical to happen before the node.login signal is
emitted. The solution presented in this PR is to set-up filters inside
messenger.Start(), which the client already calls immediately after login.

With this change, users of the mobile app can login pretty fast even when they
joined large communities. They can immediately interact with other parts of the
app even if filter initialization is running in the background, like Wallet,
Activity Center, Settings, and Profile.

Breaking changes: in the mobile repository, we had to change where the endpoint
wakuext_startMessenger was called and the order of a few events to process
chats. So essentially ordering, but no data changes.

- Root issue https://github.com/status-im/status-mobile/issues/20059
- Related mobile PR https://github.com/status-im/status-mobile/pull/20173
2024-06-10 12:02:42 -03:00

878 lines
25 KiB
Go

package ext
import (
"context"
"crypto/ecdsa"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"strings"
"time"
"github.com/syndtr/goleveldb/leveldb"
"go.uber.org/zap"
commongethtypes "github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/api/multiformat"
"github.com/status-im/status-go/connection"
"github.com/status-im/status-go/db"
coretypes "github.com/status-im/status-go/eth-node/core/types"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/anonmetrics"
"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/pushnotificationclient"
"github.com/status-im/status-go/protocol/pushnotificationserver"
"github.com/status-im/status-go/protocol/transport"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/browsers"
"github.com/status-im/status-go/services/communitytokens"
"github.com/status-im/status-go/services/ext/mailservers"
mailserversDB "github.com/status-im/status-go/services/mailservers"
"github.com/status-im/status-go/services/wallet"
"github.com/status-im/status-go/services/wallet/collectibles"
w_common "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/wakuv2"
)
const infinityString = "∞"
const providerID = "community"
// EnvelopeEventsHandler used for two different event types.
type EnvelopeEventsHandler interface {
EnvelopeSent([][]byte)
EnvelopeExpired([][]byte, error)
MailServerRequestCompleted(types.Hash, types.Hash, []byte, error)
MailServerRequestExpired(types.Hash)
}
// Service is a service that provides some additional API to whisper-based protocols like Whisper or Waku.
type Service struct {
messenger *protocol.Messenger
identity *ecdsa.PrivateKey
cancelMessenger chan struct{}
storage db.TransactionalStorage
n types.Node
rpcClient *rpc.Client
config params.NodeConfig
mailMonitor *MailRequestMonitor
server *p2p.Server
peerStore *mailservers.PeerStore
accountsDB *accounts.Database
multiAccountsDB *multiaccounts.Database
account *multiaccounts.Account
}
// Make sure that Service implements node.Service interface.
var _ node.Lifecycle = (*Service)(nil)
func New(
config params.NodeConfig,
n types.Node,
rpcClient *rpc.Client,
ldb *leveldb.DB,
mailMonitor *MailRequestMonitor,
eventSub mailservers.EnvelopeEventSubscriber,
) *Service {
cache := mailservers.NewCache(ldb)
peerStore := mailservers.NewPeerStore(cache)
return &Service{
storage: db.NewLevelDBStorage(ldb),
n: n,
rpcClient: rpcClient,
config: config,
mailMonitor: mailMonitor,
peerStore: peerStore,
}
}
func (s *Service) NodeID() *ecdsa.PrivateKey {
if s.server == nil {
return nil
}
return s.server.PrivateKey
}
func (s *Service) GetPeer(rawURL string) (*enode.Node, error) {
if len(rawURL) == 0 {
return mailservers.GetFirstConnected(s.server, s.peerStore)
}
return enode.ParseV4(rawURL)
}
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) error {
var err error
if !s.config.ShhextConfig.PFSEnabled {
return nil
}
// If Messenger has been already set up, we need to shut it down
// before we init it again. Otherwise, it will lead to goroutines leakage
// due to not stopped filters.
if s.messenger != nil {
if err := s.messenger.Shutdown(); err != nil {
return err
}
}
s.identity = identity
// This directory should have already been created in loadNodeConfig, keeping this to ensure.
dataDir := filepath.Clean(s.config.RootDataDir)
if err := os.MkdirAll(dataDir, os.ModePerm); err != nil {
return err
}
envelopesMonitorConfig := &transport.EnvelopesMonitorConfig{
MaxAttempts: s.config.ShhextConfig.MaxMessageDeliveryAttempts,
AwaitOnlyMailServerConfirmations: s.config.ShhextConfig.MailServerConfirmations,
IsMailserver: func(peer types.EnodeID) bool {
return s.peerStore.Exist(peer)
},
EnvelopeEventsHandler: EnvelopeSignalHandler{},
Logger: logger,
}
s.accountsDB, err = accounts.NewDB(appDb)
if err != nil {
return err
}
s.multiAccountsDB = multiAccountDb
s.account = acc
options, err := buildMessengerOptions(s.config, identity, appDb, walletDb, httpServer, s.rpcClient, s.multiAccountsDB, acc, envelopesMonitorConfig, s.accountsDB, walletService, communityTokensService, wakuService, logger, &MessengerSignalsHandler{}, accountManager)
if err != nil {
return err
}
messenger, err := protocol.NewMessenger(
nodeName,
identity,
s.n,
s.config.ShhextConfig.InstallationID,
s.peerStore,
options...,
)
if err != nil {
return err
}
s.messenger = messenger
s.messenger.SetP2PServer(s.server)
if s.config.ProcessBackedupMessages {
s.messenger.EnableBackedupMessagesProcessing()
}
// Be mindful of adding more initialization code, as it can easily
// impact login times for mobile users. For example, we avoid calling
// messenger.InitFilters here.
return s.messenger.InitInstallations()
}
func (s *Service) StartMessenger() (*protocol.MessengerResponse, error) {
// Start a loop that retrieves all messages and propagates them to status-mobile.
s.cancelMessenger = make(chan struct{})
response, err := s.messenger.Start()
if err != nil {
return nil, err
}
s.messenger.StartRetrieveMessagesLoop(time.Second, s.cancelMessenger)
go s.verifyTransactionLoop(30*time.Second, s.cancelMessenger)
if s.config.ShhextConfig.BandwidthStatsEnabled {
go s.retrieveStats(5*time.Second, s.cancelMessenger)
}
return response, nil
}
func (s *Service) retrieveStats(tick time.Duration, cancel <-chan struct{}) {
ticker := time.NewTicker(tick)
defer ticker.Stop()
for {
select {
case <-ticker.C:
response := s.messenger.GetStats()
PublisherSignalHandler{}.Stats(response)
case <-cancel:
return
}
}
}
type verifyTransactionClient struct {
chainID *big.Int
url string
}
func (c *verifyTransactionClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, coretypes.TransactionStatus, error) {
signer := gethtypes.NewLondonSigner(c.chainID)
client, err := ethclient.Dial(c.url)
if err != nil {
return coretypes.Message{}, coretypes.TransactionStatusPending, err
}
transaction, pending, err := client.TransactionByHash(ctx, commongethtypes.BytesToHash(hash.Bytes()))
if err != nil {
return coretypes.Message{}, coretypes.TransactionStatusPending, err
}
message, err := transaction.AsMessage(signer, nil)
if err != nil {
return coretypes.Message{}, coretypes.TransactionStatusPending, err
}
from := types.BytesToAddress(message.From().Bytes())
to := types.BytesToAddress(message.To().Bytes())
if pending {
return coretypes.NewMessage(
from,
&to,
message.Nonce(),
message.Value(),
message.Gas(),
message.GasPrice(),
message.Data(),
message.CheckNonce(),
), coretypes.TransactionStatusPending, nil
}
receipt, err := client.TransactionReceipt(ctx, commongethtypes.BytesToHash(hash.Bytes()))
if err != nil {
return coretypes.Message{}, coretypes.TransactionStatusPending, err
}
coremessage := coretypes.NewMessage(
from,
&to,
message.Nonce(),
message.Value(),
message.Gas(),
message.GasPrice(),
message.Data(),
message.CheckNonce(),
)
// Token transfer, check the logs
if len(coremessage.Data()) != 0 {
if w_common.IsTokenTransfer(receipt.Logs) {
return coremessage, coretypes.TransactionStatus(receipt.Status), nil
}
return coremessage, coretypes.TransactionStatusFailed, nil
}
return coremessage, coretypes.TransactionStatus(receipt.Status), nil
}
func (s *Service) verifyTransactionLoop(tick time.Duration, cancel <-chan struct{}) {
if s.config.ShhextConfig.VerifyTransactionURL == "" {
log.Warn("not starting transaction loop")
return
}
ticker := time.NewTicker(tick)
defer ticker.Stop()
ctx, cancelVerifyTransaction := context.WithCancel(context.Background())
for {
select {
case <-ticker.C:
accounts, err := s.accountsDB.GetActiveAccounts()
if err != nil {
log.Error("failed to retrieve accounts", "err", err)
}
var wallets []types.Address
for _, account := range accounts {
if account.IsWalletNonWatchOnlyAccount() {
wallets = append(wallets, types.BytesToAddress(account.Address.Bytes()))
}
}
response, err := s.messenger.ValidateTransactions(ctx, wallets)
if err != nil {
log.Error("failed to validate transactions", "err", err)
continue
}
s.messenger.PublishMessengerResponse(response)
case <-cancel:
cancelVerifyTransaction()
return
}
}
}
func (s *Service) EnableInstallation(installationID string) error {
return s.messenger.EnableInstallation(installationID)
}
// DisableInstallation disables an installation for multi-device sync.
func (s *Service) DisableInstallation(installationID string) error {
return s.messenger.DisableInstallation(installationID)
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []gethrpc.API {
panic("this is abstract service, use shhext or wakuext implementation")
}
func (s *Service) SetP2PServer(server *p2p.Server) {
s.server = server
}
// Start is run when a service is started.
// It does nothing in this case but is required by `node.Service` interface.
func (s *Service) Start() error {
return nil
}
// Stop is run when a service is stopped.
func (s *Service) Stop() error {
log.Info("Stopping shhext service")
if s.cancelMessenger != nil {
select {
case <-s.cancelMessenger:
// channel already closed
default:
close(s.cancelMessenger)
s.cancelMessenger = nil
}
}
if s.messenger != nil {
if err := s.messenger.Shutdown(); err != nil {
log.Error("failed to stop messenger", "err", err)
return err
}
s.messenger = nil
}
return nil
}
func buildMessengerOptions(
config params.NodeConfig,
identity *ecdsa.PrivateKey,
appDb *sql.DB,
walletDb *sql.DB,
httpServer *server.MediaServer,
rpcClient *rpc.Client,
multiAccounts *multiaccounts.Database,
account *multiaccounts.Account,
envelopesMonitorConfig *transport.EnvelopesMonitorConfig,
accountsDB *accounts.Database,
walletService *wallet.Service,
communityTokensService *communitytokens.Service,
wakuService *wakuv2.Waku,
logger *zap.Logger,
messengerSignalsHandler protocol.MessengerSignalsHandler,
accountManager account.Manager,
) ([]protocol.Option, error) {
options := []protocol.Option{
protocol.WithCustomLogger(logger),
protocol.WithPushNotifications(),
protocol.WithDatabase(appDb),
protocol.WithWalletDatabase(walletDb),
protocol.WithMultiAccounts(multiAccounts),
protocol.WithMailserversDatabase(mailserversDB.NewDB(appDb)),
protocol.WithAccount(account),
protocol.WithBrowserDatabase(browsers.NewDB(appDb)),
protocol.WithEnvelopesMonitorConfig(envelopesMonitorConfig),
protocol.WithSignalsHandler(messengerSignalsHandler),
protocol.WithENSVerificationConfig(config.ShhextConfig.VerifyENSURL, config.ShhextConfig.VerifyENSContractAddress),
protocol.WithClusterConfig(config.ClusterConfig),
protocol.WithTorrentConfig(&config.TorrentConfig),
protocol.WithHTTPServer(httpServer),
protocol.WithRPCClient(rpcClient),
protocol.WithMessageCSV(config.OutputMessageCSVEnabled),
protocol.WithWalletConfig(&config.WalletConfig),
protocol.WithWalletService(walletService),
protocol.WithCommunityTokensService(communityTokensService),
protocol.WithWakuService(wakuService),
protocol.WithAccountManager(accountManager),
}
if config.ShhextConfig.DataSyncEnabled {
options = append(options, protocol.WithDatasync())
}
settings, err := accountsDB.GetSettings()
if err != sql.ErrNoRows && err != nil {
return nil, err
}
// Generate anon metrics client config
if settings.AnonMetricsShouldSend {
keyBytes, err := hex.DecodeString(config.ShhextConfig.AnonMetricsSendID)
if err != nil {
return nil, err
}
key, err := crypto.UnmarshalPubkey(keyBytes)
if err != nil {
return nil, err
}
amcc := &anonmetrics.ClientConfig{
ShouldSend: true,
SendAddress: key,
}
options = append(options, protocol.WithAnonMetricsClientConfig(amcc))
}
// Generate anon metrics server config
if config.ShhextConfig.AnonMetricsServerEnabled {
if len(config.ShhextConfig.AnonMetricsServerPostgresURI) == 0 {
return nil, errors.New("AnonMetricsServerPostgresURI must be set")
}
amsc := &anonmetrics.ServerConfig{
Enabled: true,
PostgresURI: config.ShhextConfig.AnonMetricsServerPostgresURI,
}
options = append(options, protocol.WithAnonMetricsServerConfig(amsc))
}
if settings.TelemetryServerURL != "" {
options = append(options, protocol.WithTelemetry(settings.TelemetryServerURL))
}
if settings.PushNotificationsServerEnabled {
config := &pushnotificationserver.Config{
Enabled: true,
Logger: logger,
}
options = append(options, protocol.WithPushNotificationServerConfig(config))
}
var pushNotifServKey []*ecdsa.PublicKey
for _, d := range config.ShhextConfig.DefaultPushNotificationsServers {
pushNotifServKey = append(pushNotifServKey, d.PublicKey)
}
options = append(options, protocol.WithPushNotificationClientConfig(&pushnotificationclient.Config{
DefaultServers: pushNotifServKey,
BlockMentions: settings.PushNotificationsBlockMentions,
SendEnabled: settings.SendPushNotifications,
AllowFromContactsOnly: settings.PushNotificationsFromContactsOnly,
RemoteNotificationsEnabled: settings.RemotePushNotificationsEnabled,
}))
if config.ShhextConfig.VerifyTransactionURL != "" {
client := &verifyTransactionClient{
url: config.ShhextConfig.VerifyTransactionURL,
chainID: big.NewInt(config.ShhextConfig.VerifyTransactionChainID),
}
options = append(options, protocol.WithVerifyTransactionClient(client))
}
return options, nil
}
func (s *Service) ConnectionChanged(state connection.State) {
if s.messenger != nil {
s.messenger.ConnectionChanged(state)
}
}
func (s *Service) Messenger() *protocol.Messenger {
return s.messenger
}
func tokenURIToCommunityID(tokenURI string) string {
tmpStr := strings.Split(tokenURI, "/")
// Community NFTs have a tokenURI of the form "compressedCommunityID/tokenID"
if len(tmpStr) != 2 {
return ""
}
compressedCommunityID := tmpStr[0]
hexCommunityID, err := multiformat.DeserializeCompressedKey(compressedCommunityID)
if err != nil {
return ""
}
pubKey, err := common.HexToPubkey(hexCommunityID)
if err != nil {
return ""
}
communityID := types.EncodeHex(crypto.CompressPubkey(pubKey))
return communityID
}
func (s *Service) GetCommunityID(tokenURI string) string {
if tokenURI != "" {
return tokenURIToCommunityID(tokenURI)
}
return ""
}
func (s *Service) FillCollectiblesMetadata(communityID string, cs []*thirdparty.FullCollectibleData) (bool, error) {
if s.messenger == nil {
return false, fmt.Errorf("messenger not ready")
}
community, err := s.fetchCommunityInfoForCollectibles(communityID, collectibles.IDsFromAssets(cs))
if err != nil {
return false, err
}
if community == nil {
return false, nil
}
for _, collectible := range cs {
err := s.FillCollectibleMetadata(community, collectible)
if err != nil {
return true, err
}
}
return true, nil
}
func (s *Service) FillCollectibleMetadata(community *communities.Community, collectible *thirdparty.FullCollectibleData) error {
if s.messenger == nil {
return fmt.Errorf("messenger not ready")
}
if collectible == nil {
return fmt.Errorf("empty collectible")
}
id := collectible.CollectibleData.ID
communityID := collectible.CollectibleData.CommunityID
if communityID == "" {
return fmt.Errorf("invalid communityID")
}
tokenMetadata, err := s.fetchCommunityCollectibleMetadata(community, id.ContractID)
if err != nil {
return err
}
if tokenMetadata == nil {
return nil
}
communityToken, err := s.fetchCommunityToken(communityID, id.ContractID)
if err != nil {
return err
}
permission := fetchCommunityCollectiblePermission(community, id)
privilegesLevel := token.CommunityLevel
if permission != nil {
privilegesLevel = permissionTypeToPrivilegesLevel(permission.GetType())
}
imagePayload, _ := images.GetPayloadFromURI(tokenMetadata.GetImage())
collectible.CollectibleData.ContractType = w_common.ContractTypeERC721
collectible.CollectibleData.Provider = providerID
collectible.CollectibleData.Name = tokenMetadata.GetName()
collectible.CollectibleData.Description = tokenMetadata.GetDescription()
collectible.CollectibleData.ImagePayload = imagePayload
collectible.CollectibleData.Traits = getCollectibleCommunityTraits(communityToken)
collectible.CollectibleData.Soulbound = !communityToken.Transferable
if collectible.CollectionData == nil {
collectible.CollectionData = &thirdparty.CollectionData{
ID: id.ContractID,
CommunityID: communityID,
}
}
collectible.CollectionData.ContractType = w_common.ContractTypeERC721
collectible.CollectionData.Provider = providerID
collectible.CollectionData.Name = tokenMetadata.GetName()
collectible.CollectionData.ImagePayload = imagePayload
collectible.CommunityInfo = communityToInfo(community)
collectible.CollectibleCommunityInfo = &thirdparty.CollectibleCommunityInfo{
PrivilegesLevel: privilegesLevel,
}
return nil
}
func permissionTypeToPrivilegesLevel(permissionType protobuf.CommunityTokenPermission_Type) token.PrivilegesLevel {
switch permissionType {
case protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER:
return token.OwnerLevel
case protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER:
return token.MasterLevel
default:
return token.CommunityLevel
}
}
func communityToInfo(community *communities.Community) *thirdparty.CommunityInfo {
if community == nil {
return nil
}
return &thirdparty.CommunityInfo{
CommunityName: community.Name(),
CommunityColor: community.Color(),
CommunityImagePayload: fetchCommunityImage(community),
}
}
func (s *Service) fetchCommunityFromStoreNodes(communityID string) (*communities.Community, error) {
community, err := s.messenger.FetchCommunity(&protocol.FetchCommunityRequest{
CommunityKey: communityID,
TryDatabase: false,
WaitForResponse: true,
})
if err != nil {
return nil, err
}
return community, nil
}
// Fetch latest community from store nodes.
func (s *Service) FetchCommunityInfo(communityID string) (*thirdparty.CommunityInfo, error) {
if s.messenger == nil {
return nil, fmt.Errorf("messenger not ready")
}
community, err := s.messenger.FindCommunityInfoFromDB(communityID)
if err != nil && err != communities.ErrOrgNotFound {
return nil, err
}
// Fetch latest version from store nodes
if community == nil || !community.IsControlNode() {
community, err = s.fetchCommunityFromStoreNodes(communityID)
if err != nil {
return nil, err
}
}
return communityToInfo(community), nil
}
// Fetch latest community from store nodes only if any collectibles data is missing.
func (s *Service) fetchCommunityInfoForCollectibles(communityID string, ids []thirdparty.CollectibleUniqueID) (*communities.Community, error) {
community, err := s.messenger.FindCommunityInfoFromDB(communityID)
if err != nil && err != communities.ErrOrgNotFound {
return nil, err
}
if community == nil {
return s.fetchCommunityFromStoreNodes(communityID)
}
if community.IsControlNode() {
return community, nil
}
contractIDs := func() map[string]thirdparty.ContractID {
result := map[string]thirdparty.ContractID{}
for _, id := range ids {
result[id.HashKey()] = id.ContractID
}
return result
}()
hasAllMetadata := true
for _, contractID := range contractIDs {
tokenMetadata, err := s.fetchCommunityCollectibleMetadata(community, contractID)
if err != nil {
return nil, err
}
if tokenMetadata == nil {
hasAllMetadata = false
break
}
}
if !hasAllMetadata {
return s.fetchCommunityFromStoreNodes(communityID)
}
return community, nil
}
func (s *Service) fetchCommunityToken(communityID string, contractID thirdparty.ContractID) (*token.CommunityToken, error) {
if s.messenger == nil {
return nil, fmt.Errorf("messenger not ready")
}
return s.messenger.GetCommunityToken(communityID, int(contractID.ChainID), contractID.Address.String())
}
func (s *Service) fetchCommunityCollectibleMetadata(community *communities.Community, contractID thirdparty.ContractID) (*protobuf.CommunityTokenMetadata, error) {
tokensMetadata := community.CommunityTokensMetadata()
for _, tokenMetadata := range tokensMetadata {
contractAddresses := tokenMetadata.GetContractAddresses()
if contractAddresses[uint64(contractID.ChainID)] == contractID.Address.Hex() {
return tokenMetadata, nil
}
}
return nil, nil
}
func tokenCriterionContainsCollectible(tokenCriterion *protobuf.TokenCriteria, id thirdparty.CollectibleUniqueID) bool {
// Check if token type matches
if tokenCriterion.Type != protobuf.CommunityTokenType_ERC721 {
return false
}
for chainID, contractAddressStr := range tokenCriterion.ContractAddresses {
if chainID != uint64(id.ContractID.ChainID) {
continue
}
contractAddress := commongethtypes.HexToAddress(contractAddressStr)
if contractAddress != id.ContractID.Address {
continue
}
if len(tokenCriterion.TokenIds) == 0 {
return true
}
for _, tokenID := range tokenCriterion.TokenIds {
tokenIDBigInt := new(big.Int).SetUint64(tokenID)
if id.TokenID.Cmp(tokenIDBigInt) == 0 {
return true
}
}
}
return false
}
func permissionContainsCollectible(permission *communities.CommunityTokenPermission, id thirdparty.CollectibleUniqueID) bool {
// See if any token criterion contains the collectible we're looking for
for _, tokenCriterion := range permission.TokenCriteria {
if tokenCriterionContainsCollectible(tokenCriterion, id) {
return true
}
}
return false
}
func fetchCommunityCollectiblePermission(community *communities.Community, id thirdparty.CollectibleUniqueID) *communities.CommunityTokenPermission {
// Permnission types of interest
permissionTypes := []protobuf.CommunityTokenPermission_Type{
protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER,
protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER,
}
for _, permissionType := range permissionTypes {
permissions := community.TokenPermissionsByType(permissionType)
// See if any community permission matches the type we're looking for
for _, permission := range permissions {
if permissionContainsCollectible(permission, id) {
return permission
}
}
}
return nil
}
func fetchCommunityImage(community *communities.Community) []byte {
imageTypes := []string{
images.LargeDimName,
images.SmallDimName,
}
communityImages := community.Images()
for _, imageType := range imageTypes {
if pbImage, ok := communityImages[imageType]; ok {
return pbImage.Payload
}
}
return nil
}
func boolToString(value bool) string {
if value {
return "Yes"
}
return "No"
}
func getCollectibleCommunityTraits(token *token.CommunityToken) []thirdparty.CollectibleTrait {
if token == nil {
return make([]thirdparty.CollectibleTrait, 0)
}
totalStr := infinityString
availableStr := infinityString
if !token.InfiniteSupply {
totalStr = token.Supply.String()
// TODO: calculate available supply. See services/communitytokens/api.go
availableStr = totalStr
}
transferableStr := boolToString(token.Transferable)
destructibleStr := boolToString(token.RemoteSelfDestruct)
return []thirdparty.CollectibleTrait{
{
TraitType: "Symbol",
Value: token.Symbol,
},
{
TraitType: "Total",
Value: totalStr,
},
{
TraitType: "Available",
Value: availableStr,
},
{
TraitType: "Transferable",
Value: transferableStr,
},
{
TraitType: "Destructible",
Value: destructibleStr,
},
}
}