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 }