feat: lazy load collectibles metadata
This commit is contained in:
parent
e5ce11f067
commit
959dcbdea5
|
@ -59,6 +59,7 @@ import (
|
|||
)
|
||||
|
||||
const infinityString = "∞"
|
||||
const providerID = "community"
|
||||
|
||||
// EnvelopeEventsHandler used for two different event types.
|
||||
type EnvelopeEventsHandler interface {
|
||||
|
@ -548,7 +549,7 @@ func (s *Service) FillCollectibleMetadata(collectible *thirdparty.FullCollectibl
|
|||
return fmt.Errorf("invalid communityID")
|
||||
}
|
||||
|
||||
community, err := s.fetchCommunity(communityID)
|
||||
community, err := s.fetchCommunity(communityID, true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -582,6 +583,7 @@ func (s *Service) FillCollectibleMetadata(collectible *thirdparty.FullCollectibl
|
|||
|
||||
imagePayload, _ := images.GetPayloadFromURI(tokenMetadata.GetImage())
|
||||
|
||||
collectible.CollectibleData.Provider = providerID
|
||||
collectible.CollectibleData.Name = tokenMetadata.GetName()
|
||||
collectible.CollectibleData.Description = tokenMetadata.GetDescription()
|
||||
collectible.CollectibleData.ImagePayload = imagePayload
|
||||
|
@ -593,10 +595,13 @@ func (s *Service) FillCollectibleMetadata(collectible *thirdparty.FullCollectibl
|
|||
CommunityID: communityID,
|
||||
}
|
||||
}
|
||||
collectible.CollectionData.Provider = providerID
|
||||
collectible.CollectionData.Name = tokenMetadata.GetName()
|
||||
collectible.CollectionData.ImagePayload = imagePayload
|
||||
|
||||
collectible.CommunityInfo = &thirdparty.CollectibleCommunityInfo{
|
||||
collectible.CommunityInfo = communityToInfo(community)
|
||||
|
||||
collectible.CollectibleCommunityInfo = &thirdparty.CollectibleCommunityInfo{
|
||||
PrivilegesLevel: privilegesLevel,
|
||||
}
|
||||
|
||||
|
@ -614,25 +619,28 @@ func permissionTypeToPrivilegesLevel(permissionType protobuf.CommunityTokenPermi
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) FetchCommunityInfo(communityID string) (*thirdparty.CommunityInfo, error) {
|
||||
community, err := s.fetchCommunity(communityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func communityToInfo(community *communities.Community) *thirdparty.CommunityInfo {
|
||||
if community == nil {
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
communityInfo := &thirdparty.CommunityInfo{
|
||||
return &thirdparty.CommunityInfo{
|
||||
CommunityName: community.Name(),
|
||||
CommunityColor: community.Color(),
|
||||
CommunityImagePayload: fetchCommunityImage(community),
|
||||
}
|
||||
|
||||
return communityInfo, nil
|
||||
}
|
||||
|
||||
func (s *Service) fetchCommunity(communityID string) (*communities.Community, error) {
|
||||
func (s *Service) FetchCommunityInfo(communityID string) (*thirdparty.CommunityInfo, error) {
|
||||
community, err := s.fetchCommunity(communityID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return communityToInfo(community), nil
|
||||
}
|
||||
|
||||
func (s *Service) fetchCommunity(communityID string, tryDatabase bool) (*communities.Community, error) {
|
||||
if s.messenger == nil {
|
||||
return nil, fmt.Errorf("messenger not ready")
|
||||
}
|
||||
|
@ -646,7 +654,7 @@ func (s *Service) fetchCommunity(communityID string) (*communities.Community, er
|
|||
community, err := s.messenger.FetchCommunity(&protocol.FetchCommunityRequest{
|
||||
CommunityKey: communityID,
|
||||
Shard: shard,
|
||||
TryDatabase: true,
|
||||
TryDatabase: tryDatabase,
|
||||
WaitForResponse: true,
|
||||
})
|
||||
|
||||
|
|
|
@ -153,7 +153,7 @@ func (s *Service) GetActivityCollectiblesAsync(requestID int32, chainIDs []w_com
|
|||
return nil, err
|
||||
}
|
||||
|
||||
data, err := s.collectibles.FetchAssetsByCollectibleUniqueID(ctx, collectibles)
|
||||
data, err := s.collectibles.FetchAssetsByCollectibleUniqueID(ctx, collectibles, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -222,7 +222,7 @@ func (s *Service) getActivityDetails(ctx context.Context, entries []Entry) ([]*E
|
|||
|
||||
log.Debug("wallet.activity.Service lazyLoadDetails", "entries.len", len(entries), "ids.len", len(ids))
|
||||
|
||||
colData, err := s.collectibles.FetchAssetsByCollectibleUniqueID(ctx, ids)
|
||||
colData, err := s.collectibles.FetchAssetsByCollectibleUniqueID(ctx, ids, true)
|
||||
if err != nil {
|
||||
log.Error("Error fetching collectible details", "error", err)
|
||||
return nil, err
|
||||
|
|
|
@ -29,7 +29,7 @@ type mockCollectiblesManager struct {
|
|||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockCollectiblesManager) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
||||
func (m *mockCollectiblesManager) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID, asyncFetch bool) ([]thirdparty.FullCollectibleData, error) {
|
||||
args := m.Called(uniqueIDs)
|
||||
res := args.Get(0)
|
||||
if res == nil {
|
||||
|
|
|
@ -336,24 +336,6 @@ func (api *API) GetCollectiblesByUniqueIDAsync(requestID int32, uniqueIDs []thir
|
|||
return nil
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
func (api *API) GetCollectiblesByOwnerWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
log.Debug("call to GetCollectiblesByOwnerWithCursor")
|
||||
return api.s.collectiblesManager.FetchAllAssetsByOwner(ctx, chainID, owner, cursor, limit, thirdparty.FetchFromAnyProvider)
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
func (api *API) GetCollectiblesByOwnerAndContractAddressWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
log.Debug("call to GetCollectiblesByOwnerAndContractAddressWithCursor")
|
||||
return api.s.collectiblesManager.FetchAllAssetsByOwnerAndContractAddress(ctx, chainID, owner, contractAddresses, cursor, limit, thirdparty.FetchFromAnyProvider)
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
func (api *API) GetCollectiblesByUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
||||
log.Debug("call to GetCollectiblesByUniqueID")
|
||||
return api.s.collectiblesManager.FetchAssetsByCollectibleUniqueID(ctx, uniqueIDs)
|
||||
}
|
||||
|
||||
func (api *API) GetCollectibleOwnersByContractAddress(ctx context.Context, chainID wcommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
|
||||
log.Debug("call to GetCollectibleOwnersByContractAddress")
|
||||
return api.s.collectiblesManager.FetchCollectibleOwnersByContractAddress(ctx, chainID, contractAddress)
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package collectibles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/status-im/status-go/protocol/communities/token"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
w_common "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
|
@ -25,74 +23,11 @@ func setupCollectibleDataDBTest(t *testing.T) (*CollectibleDataDB, func()) {
|
|||
}
|
||||
}
|
||||
|
||||
func generateTestCollectiblesData(count int) (result []thirdparty.CollectibleData) {
|
||||
result = make([]thirdparty.CollectibleData, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
bigI := big.NewInt(int64(i))
|
||||
newCollectible := thirdparty.CollectibleData{
|
||||
ID: thirdparty.CollectibleUniqueID{
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: w_common.ChainID(i % 4),
|
||||
Address: common.BigToAddress(bigI),
|
||||
},
|
||||
TokenID: &bigint.BigInt{Int: bigI},
|
||||
},
|
||||
Provider: fmt.Sprintf("provider-%d", i),
|
||||
Name: fmt.Sprintf("name-%d", i),
|
||||
Description: fmt.Sprintf("description-%d", i),
|
||||
Permalink: fmt.Sprintf("permalink-%d", i),
|
||||
ImageURL: fmt.Sprintf("imageurl-%d", i),
|
||||
ImagePayload: []byte(fmt.Sprintf("imagepayload-%d", i)),
|
||||
AnimationURL: fmt.Sprintf("animationurl-%d", i),
|
||||
AnimationMediaType: fmt.Sprintf("animationmediatype-%d", i),
|
||||
Traits: []thirdparty.CollectibleTrait{
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
},
|
||||
BackgroundColor: fmt.Sprintf("backgroundcolor-%d", i),
|
||||
TokenURI: fmt.Sprintf("tokenuri-%d", i),
|
||||
CommunityID: fmt.Sprintf("communityid-%d", i%5),
|
||||
}
|
||||
if i%5 == 0 {
|
||||
newCollectible.CommunityID = ""
|
||||
}
|
||||
result = append(result, newCollectible)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func generateTestCommunityData(count int) []thirdparty.CollectibleCommunityInfo {
|
||||
result := make([]thirdparty.CollectibleCommunityInfo, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
newCommunityInfo := thirdparty.CollectibleCommunityInfo{
|
||||
PrivilegesLevel: token.PrivilegesLevel(i) % (token.CommunityLevel + 1),
|
||||
}
|
||||
result = append(result, newCommunityInfo)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestUpdateCollectiblesData(t *testing.T) {
|
||||
db, cleanDB := setupCollectibleDataDBTest(t)
|
||||
defer cleanDB()
|
||||
|
||||
data := generateTestCollectiblesData(50)
|
||||
data := thirdparty.GenerateTestCollectiblesData(50)
|
||||
|
||||
var err error
|
||||
|
||||
|
@ -168,8 +103,8 @@ func TestUpdateCommunityData(t *testing.T) {
|
|||
defer cleanDB()
|
||||
|
||||
const nData = 50
|
||||
data := generateTestCollectiblesData(nData)
|
||||
communityData := generateTestCommunityData(nData)
|
||||
data := thirdparty.GenerateTestCollectiblesData(nData)
|
||||
communityData := thirdparty.GenerateTestCollectiblesCommunityData(nData)
|
||||
|
||||
var err error
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package collectibles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
|
@ -23,41 +22,11 @@ func setupCollectionDataDBTest(t *testing.T) (*CollectionDataDB, func()) {
|
|||
}
|
||||
}
|
||||
|
||||
func generateTestCollectionsData(count int) (result []thirdparty.CollectionData) {
|
||||
result = make([]thirdparty.CollectionData, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
bigI := big.NewInt(int64(count))
|
||||
traits := make(map[string]thirdparty.CollectionTrait)
|
||||
for j := 0; j < 3; j++ {
|
||||
traits[fmt.Sprintf("traittype-%d", j)] = thirdparty.CollectionTrait{
|
||||
Min: float64(i+j) / 2,
|
||||
Max: float64(i+j) * 2,
|
||||
}
|
||||
}
|
||||
|
||||
newCollection := thirdparty.CollectionData{
|
||||
ID: thirdparty.ContractID{
|
||||
ChainID: w_common.ChainID(i),
|
||||
Address: common.BigToAddress(bigI),
|
||||
},
|
||||
Provider: fmt.Sprintf("provider-%d", i),
|
||||
Name: fmt.Sprintf("name-%d", i),
|
||||
Slug: fmt.Sprintf("slug-%d", i),
|
||||
ImageURL: fmt.Sprintf("imageurl-%d", i),
|
||||
ImagePayload: []byte(fmt.Sprintf("imagepayload-%d", i)),
|
||||
Traits: traits,
|
||||
CommunityID: fmt.Sprintf("community-%d", i),
|
||||
}
|
||||
result = append(result, newCollection)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestUpdateCollectionsData(t *testing.T) {
|
||||
db, cleanDB := setupCollectionDataDBTest(t)
|
||||
defer cleanDB()
|
||||
|
||||
data := generateTestCollectionsData(50)
|
||||
data := thirdparty.GenerateTestCollectionsData(50)
|
||||
|
||||
var err error
|
||||
|
||||
|
|
|
@ -35,8 +35,8 @@ func TestFilterOwnedCollectibles(t *testing.T) {
|
|||
cDB := NewCollectibleDataDB(db)
|
||||
|
||||
const nData = 50
|
||||
data := generateTestCollectiblesData(nData)
|
||||
communityData := generateTestCommunityData(nData)
|
||||
data := thirdparty.GenerateTestCollectiblesData(nData)
|
||||
communityData := thirdparty.GenerateTestCollectiblesCommunityData(nData)
|
||||
|
||||
ownerAddresses := []common.Address{
|
||||
common.HexToAddress("0x1234"),
|
||||
|
|
|
@ -3,6 +3,7 @@ package collectibles
|
|||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/big"
|
||||
"net/http"
|
||||
|
@ -18,6 +19,7 @@ import (
|
|||
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
"github.com/status-im/status-go/server"
|
||||
"github.com/status-im/status-go/services/wallet/async"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/community"
|
||||
|
@ -27,6 +29,7 @@ import (
|
|||
)
|
||||
|
||||
const requestTimeout = 5 * time.Second
|
||||
const signalUpdatedCollectiblesDataPageSize = 10
|
||||
|
||||
const hystrixContractOwnershipClientName = "contractOwnershipClient"
|
||||
|
||||
|
@ -45,7 +48,7 @@ var (
|
|||
)
|
||||
|
||||
type ManagerInterface interface {
|
||||
FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error)
|
||||
FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID, asyncFetch bool) ([]thirdparty.FullCollectibleData, error)
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
|
@ -61,11 +64,13 @@ type Manager struct {
|
|||
collectiblesDataDB *CollectibleDataDB
|
||||
collectionsDataDB *CollectionDataDB
|
||||
communityManager *community.Manager
|
||||
ownershipDB *OwnershipDB
|
||||
|
||||
mediaServer *server.MediaServer
|
||||
|
||||
statuses map[string]*connection.Status
|
||||
statusNotifier *connection.StatusNotifier
|
||||
feed *event.Feed
|
||||
}
|
||||
|
||||
func NewManager(
|
||||
|
@ -139,9 +144,11 @@ func NewManager(
|
|||
collectiblesDataDB: NewCollectibleDataDB(db),
|
||||
collectionsDataDB: NewCollectionDataDB(db),
|
||||
communityManager: communityManager,
|
||||
ownershipDB: ownershipDB,
|
||||
mediaServer: mediaServer,
|
||||
statuses: statuses,
|
||||
statusNotifier: statusNotifier,
|
||||
feed: feed,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -262,7 +269,7 @@ func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(ctx context.Context, c
|
|||
continue
|
||||
}
|
||||
|
||||
err = o.processFullCollectibleData(ctx, assetContainer.Items)
|
||||
_, err = o.processFullCollectibleData(ctx, assetContainer.Items, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -295,7 +302,7 @@ func (o *Manager) FetchAllAssetsByOwner(ctx context.Context, chainID walletCommo
|
|||
continue
|
||||
}
|
||||
|
||||
err = o.processFullCollectibleData(ctx, assetContainer.Items)
|
||||
_, err = o.processFullCollectibleData(ctx, assetContainer.Items, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -321,7 +328,10 @@ func (o *Manager) FetchCollectibleOwnershipByOwner(ctx context.Context, chainID
|
|||
return &ret, nil
|
||||
}
|
||||
|
||||
func (o *Manager) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
||||
// Returns collectible metadata for the given unique IDs.
|
||||
// If asyncFetch is true, empty metadata will be returned for any missing collectibles and an EventCollectiblesDataUpdated will be sent when the data is ready.
|
||||
// If asyncFetch is false, it will wait for all collectibles' metadata to be retrieved before returning.
|
||||
func (o *Manager) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID, asyncFetch bool) ([]thirdparty.FullCollectibleData, error) {
|
||||
missingIDs, err := o.collectiblesDataDB.GetIDsNotInDB(uniqueIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -329,27 +339,40 @@ func (o *Manager) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueID
|
|||
|
||||
missingIDsPerChainID := thirdparty.GroupCollectibleUIDsByChainID(missingIDs)
|
||||
|
||||
for chainID, idsToFetch := range missingIDsPerChainID {
|
||||
defer o.checkConnectionStatus(chainID)
|
||||
group := async.NewGroup(ctx)
|
||||
group.Add(func(ctx context.Context) error {
|
||||
for chainID, idsToFetch := range missingIDsPerChainID {
|
||||
defer o.checkConnectionStatus(chainID)
|
||||
|
||||
for _, provider := range o.collectibleDataProviders {
|
||||
if !provider.IsChainSupported(chainID) {
|
||||
continue
|
||||
for _, provider := range o.collectibleDataProviders {
|
||||
if !provider.IsChainSupported(chainID) {
|
||||
continue
|
||||
}
|
||||
|
||||
fetchedAssets, err := provider.FetchAssetsByCollectibleUniqueID(ctx, idsToFetch)
|
||||
if err != nil {
|
||||
log.Error("FetchAssetsByCollectibleUniqueID failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
updatedCollectibles, err := o.processFullCollectibleData(ctx, fetchedAssets, asyncFetch)
|
||||
if err != nil {
|
||||
log.Error("processFullCollectibleData failed for", "provider", provider.ID(), "chainID", chainID, "len(fetchedAssets)", len(fetchedAssets), "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if asyncFetch {
|
||||
o.signalUpdatedCollectiblesData(updatedCollectibles)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
fetchedAssets, err := provider.FetchAssetsByCollectibleUniqueID(ctx, idsToFetch)
|
||||
if err != nil {
|
||||
log.Error("FetchAssetsByCollectibleUniqueID failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = o.processFullCollectibleData(ctx, fetchedAssets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if !asyncFetch {
|
||||
group.Wait()
|
||||
}
|
||||
|
||||
return o.getCacheFullCollectibleData(uniqueIDs)
|
||||
|
@ -445,12 +468,6 @@ func (o *Manager) FetchCollectibleOwnersByContractAddress(ctx context.Context, c
|
|||
return owners.(*thirdparty.CollectibleContractOwnership), nil
|
||||
}
|
||||
|
||||
func isMetadataEmpty(asset thirdparty.CollectibleData) bool {
|
||||
return asset.Name == "" &&
|
||||
asset.Description == "" &&
|
||||
asset.ImageURL == ""
|
||||
}
|
||||
|
||||
func (o *Manager) fetchTokenURI(ctx context.Context, id thirdparty.CollectibleUniqueID) (string, error) {
|
||||
if id.TokenID == nil {
|
||||
return "", errors.New("empty token ID")
|
||||
|
@ -482,9 +499,18 @@ func (o *Manager) fetchTokenURI(ctx context.Context, id thirdparty.CollectibleUn
|
|||
return tokenURI, err
|
||||
}
|
||||
|
||||
func (o *Manager) processFullCollectibleData(ctx context.Context, assets []thirdparty.FullCollectibleData) error {
|
||||
func isMetadataEmpty(asset thirdparty.CollectibleData) bool {
|
||||
return asset.Description == "" &&
|
||||
asset.ImageURL == ""
|
||||
}
|
||||
|
||||
// Processes collectible metadata obtained from a provider and ensures any missing data is fetched.
|
||||
// If asyncFetch is true, community collectibles metadata will be fetched async and an EventCollectiblesDataUpdated will be sent when the data is ready.
|
||||
// If asyncFetch is false, it will wait for all community collectibles' metadata to be retrieved before returning.
|
||||
func (o *Manager) processFullCollectibleData(ctx context.Context, assets []thirdparty.FullCollectibleData, asyncFetch bool) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
fullyFetchedAssets := make(map[string]*thirdparty.FullCollectibleData)
|
||||
communityCollectibles := make(map[string][]*thirdparty.FullCollectibleData)
|
||||
processedIDs := make([]thirdparty.CollectibleUniqueID, 0, len(assets))
|
||||
|
||||
// Start with all assets, remove if any of the fetch steps fail
|
||||
for idx := range assets {
|
||||
|
@ -493,15 +519,19 @@ func (o *Manager) processFullCollectibleData(ctx context.Context, assets []third
|
|||
fullyFetchedAssets[id.HashKey()] = asset
|
||||
}
|
||||
|
||||
// Detect community collectibles
|
||||
for _, asset := range fullyFetchedAssets {
|
||||
// Only check community ownership if metadata is empty
|
||||
if isMetadataEmpty(asset.CollectibleData) {
|
||||
// Get TokenURI if not given by provider
|
||||
err := o.fillTokenURI(ctx, asset)
|
||||
if err != nil {
|
||||
log.Error("fillTokenURI failed", "err", err)
|
||||
delete(fullyFetchedAssets, asset.CollectibleData.ID.HashKey())
|
||||
continue
|
||||
}
|
||||
|
||||
// Get CommunityID if obtainable from TokenURI
|
||||
err = o.fillCommunityID(asset)
|
||||
if err != nil {
|
||||
log.Error("fillCommunityID failed", "err", err)
|
||||
|
@ -509,25 +539,32 @@ func (o *Manager) processFullCollectibleData(ctx context.Context, assets []third
|
|||
continue
|
||||
}
|
||||
|
||||
// Get metadata from community if community collectible
|
||||
communityID := asset.CollectibleData.CommunityID
|
||||
if communityID != "" {
|
||||
if _, ok := communityCollectibles[communityID]; !ok {
|
||||
communityCollectibles[communityID] = make([]*thirdparty.FullCollectibleData, 0)
|
||||
}
|
||||
communityCollectibles[communityID] = append(communityCollectibles[communityID], asset)
|
||||
|
||||
// Community collectibles are handled separately, remove from list
|
||||
delete(fullyFetchedAssets, asset.CollectibleData.ID.HashKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Community collectibles are grouped by community ID
|
||||
// If fetching data for one community fails (for example, owner node is down),
|
||||
// skip and continue with the other communities.
|
||||
for communityID, communityAssets := range communityCollectibles {
|
||||
err := o.fillCommunityInfo(communityID, communityAssets)
|
||||
if err != nil {
|
||||
log.Error("fillCommunityInfo failed", "communityID", communityID, "err", err)
|
||||
for _, communityAsset := range communityAssets {
|
||||
delete(fullyFetchedAssets, communityAsset.CollectibleData.ID.HashKey())
|
||||
if asyncFetch {
|
||||
o.fetchCommunityAssetsAsync(ctx, communityID, communityAssets)
|
||||
} else {
|
||||
err := o.fetchCommunityAssets(communityID, communityAssets)
|
||||
if err != nil {
|
||||
log.Error("fetchCommunityAssets failed", "communityID", communityID, "err", err)
|
||||
continue
|
||||
}
|
||||
for _, asset := range communityAssets {
|
||||
processedIDs = append(processedIDs, asset.CollectibleData.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -548,6 +585,7 @@ func (o *Manager) processFullCollectibleData(ctx context.Context, assets []third
|
|||
|
||||
for _, asset := range fullyFetchedAssets {
|
||||
id := asset.CollectibleData.ID
|
||||
processedIDs = append(processedIDs, id)
|
||||
|
||||
collectiblesData = append(collectiblesData, asset.CollectibleData)
|
||||
if asset.CollectionData != nil {
|
||||
|
@ -559,32 +597,23 @@ func (o *Manager) processFullCollectibleData(ctx context.Context, assets []third
|
|||
|
||||
err := o.collectiblesDataDB.SetData(collectiblesData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, asset := range assets {
|
||||
if asset.CommunityInfo != nil {
|
||||
err = o.collectiblesDataDB.SetCommunityInfo(asset.CollectibleData.ID, *asset.CommunityInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = o.collectionsDataDB.SetData(collectionsData)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(missingCollectionIDs) > 0 {
|
||||
// Calling this ensures collection data is fetched and cached (if not already available)
|
||||
_, err := o.FetchCollectionsDataByContractID(ctx, missingCollectionIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return processedIDs, nil
|
||||
}
|
||||
|
||||
func (o *Manager) fillTokenURI(ctx context.Context, asset *thirdparty.FullCollectibleData) error {
|
||||
|
@ -616,9 +645,10 @@ func (o *Manager) fillCommunityID(asset *thirdparty.FullCollectibleData) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (o *Manager) fillCommunityInfo(communityID string, communityAssets []*thirdparty.FullCollectibleData) error {
|
||||
func (o *Manager) fetchCommunityAssets(communityID string, communityAssets []*thirdparty.FullCollectibleData) error {
|
||||
communityInfo, err := o.communityManager.FetchCommunityInfo(communityID)
|
||||
if err != nil {
|
||||
log.Error("fetchCommunityInfo failed", "communityID", communityID, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -626,6 +656,41 @@ func (o *Manager) fillCommunityInfo(communityID string, communityAssets []*third
|
|||
for _, communityAsset := range communityAssets {
|
||||
err := o.communityManager.FillCollectibleMetadata(communityAsset)
|
||||
if err != nil {
|
||||
log.Error("FillCollectibleMetadata failed", "communityID", communityID, "err", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Warn("fetchCommunityAssets community not found", "communityID", communityID)
|
||||
}
|
||||
|
||||
collectiblesData := make([]thirdparty.CollectibleData, 0, len(communityAssets))
|
||||
collectionsData := make([]thirdparty.CollectionData, 0, len(communityAssets))
|
||||
|
||||
for _, asset := range communityAssets {
|
||||
collectiblesData = append(collectiblesData, asset.CollectibleData)
|
||||
if asset.CollectionData != nil {
|
||||
collectionsData = append(collectionsData, *asset.CollectionData)
|
||||
}
|
||||
}
|
||||
|
||||
err = o.collectiblesDataDB.SetData(collectiblesData)
|
||||
if err != nil {
|
||||
log.Error("collectiblesDataDB SetData failed", "communityID", communityID, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = o.collectionsDataDB.SetData(collectionsData)
|
||||
if err != nil {
|
||||
log.Error("collectionsDataDB SetData failed", "communityID", communityID, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, asset := range communityAssets {
|
||||
if asset.CollectibleCommunityInfo != nil {
|
||||
err = o.collectiblesDataDB.SetCommunityInfo(asset.CollectibleData.ID, *asset.CollectibleCommunityInfo)
|
||||
if err != nil {
|
||||
log.Error("collectiblesDataDB SetCommunityInfo failed", "communityID", communityID, "err", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -634,6 +699,27 @@ func (o *Manager) fillCommunityInfo(communityID string, communityAssets []*third
|
|||
return nil
|
||||
}
|
||||
|
||||
func (o *Manager) fetchCommunityAssetsAsync(ctx context.Context, communityID string, communityAssets []*thirdparty.FullCollectibleData) {
|
||||
if len(communityAssets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := o.fetchCommunityAssets(communityID, communityAssets)
|
||||
if err != nil {
|
||||
log.Error("fetchCommunityAssets failed", "communityID", communityID, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Metadata is up to date in db at this point, fetch and send Event.
|
||||
ids := make([]thirdparty.CollectibleUniqueID, 0, len(communityAssets))
|
||||
for _, asset := range communityAssets {
|
||||
ids = append(ids, asset.CollectibleData.ID)
|
||||
}
|
||||
o.signalUpdatedCollectiblesData(ids)
|
||||
}()
|
||||
}
|
||||
|
||||
func (o *Manager) fillAnimationMediatype(ctx context.Context, asset *thirdparty.FullCollectibleData) error {
|
||||
if len(asset.CollectibleData.AnimationURL) > 0 {
|
||||
contentType, err := o.doContentTypeRequest(ctx, asset.CollectibleData.AnimationURL)
|
||||
|
@ -690,15 +776,27 @@ func (o *Manager) getCacheFullCollectibleData(uniqueIDs []thirdparty.Collectible
|
|||
collectionData.ImageURL = o.mediaServer.MakeWalletCollectionImagesURL(collectionData.ID)
|
||||
}
|
||||
|
||||
communityInfo, err := o.collectiblesDataDB.GetCommunityInfo(id)
|
||||
communityInfo, _, err := o.communityManager.GetCommunityInfo(collectibleData.CommunityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collectibleCommunityInfo, err := o.collectiblesDataDB.GetCommunityInfo(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownership, err := o.ownershipDB.GetOwnership(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fullData := thirdparty.FullCollectibleData{
|
||||
CollectibleData: collectibleData,
|
||||
CollectionData: &collectionData,
|
||||
CommunityInfo: communityInfo,
|
||||
CollectibleData: collectibleData,
|
||||
CollectionData: &collectionData,
|
||||
CommunityInfo: communityInfo,
|
||||
CollectibleCommunityInfo: collectibleCommunityInfo,
|
||||
Ownership: ownership,
|
||||
}
|
||||
ret = append(ret, fullData)
|
||||
}
|
||||
|
@ -723,3 +821,36 @@ func (o *Manager) checkConnectionStatus(chainID walletCommon.ChainID) {
|
|||
}
|
||||
o.statuses[chainID.String()].SetIsConnected(false)
|
||||
}
|
||||
|
||||
func (o *Manager) signalUpdatedCollectiblesData(ids []thirdparty.CollectibleUniqueID) {
|
||||
// We limit how much collectibles data we send in each event to avoid problems on the client side
|
||||
for startIdx := 0; startIdx < len(ids); startIdx += signalUpdatedCollectiblesDataPageSize {
|
||||
endIdx := startIdx + signalUpdatedCollectiblesDataPageSize
|
||||
if endIdx > len(ids) {
|
||||
endIdx = len(ids)
|
||||
}
|
||||
pageIDs := ids[startIdx:endIdx]
|
||||
|
||||
collectibles, err := o.getCacheFullCollectibleData(pageIDs)
|
||||
if err != nil {
|
||||
log.Error("Error getting FullCollectibleData from cache: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Send update event with most complete data type available
|
||||
details := fullCollectiblesDataToDetails(collectibles)
|
||||
|
||||
payload, err := json.Marshal(details)
|
||||
if err != nil {
|
||||
log.Error("Error marshaling response: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
event := walletevent.Event{
|
||||
Type: EventCollectiblesDataUpdated,
|
||||
Message: string(payload),
|
||||
}
|
||||
|
||||
o.feed.Send(event)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ const (
|
|||
EventCollectiblesOwnershipUpdateFinished walletevent.EventType = "wallet-collectibles-ownership-update-finished"
|
||||
EventCollectiblesOwnershipUpdateFinishedWithError walletevent.EventType = "wallet-collectibles-ownership-update-finished-with-error"
|
||||
EventCommunityCollectiblesReceived walletevent.EventType = "wallet-collectibles-community-collectibles-received"
|
||||
EventCollectiblesDataUpdated walletevent.EventType = "wallet-collectibles-data-updated"
|
||||
|
||||
EventOwnedCollectiblesFilteringDone walletevent.EventType = "wallet-owned-collectibles-filtering-done"
|
||||
EventGetCollectiblesDetailsDone walletevent.EventType = "wallet-get-collectibles-details-done"
|
||||
|
@ -76,6 +77,7 @@ type Service struct {
|
|||
communityManager *community.Manager
|
||||
walletFeed *event.Feed
|
||||
scheduler *async.MultiClientScheduler
|
||||
group *async.Group
|
||||
}
|
||||
|
||||
func NewService(
|
||||
|
@ -95,6 +97,7 @@ func NewService(
|
|||
communityManager: communityManager,
|
||||
walletFeed: walletFeed,
|
||||
scheduler: async.NewMultiClientScheduler(),
|
||||
group: async.NewGroup(context.Background()),
|
||||
}
|
||||
s.controller.SetReceivedCollectiblesCb(s.notifyCommunityCollectiblesReceived)
|
||||
return s
|
||||
|
@ -380,136 +383,32 @@ func (s *Service) collectibleIDsToDataType(ctx context.Context, ids []thirdparty
|
|||
case CollectibleDataTypeUniqueID:
|
||||
return idsToCollectibles(ids), nil
|
||||
case CollectibleDataTypeHeader, CollectibleDataTypeDetails, CollectibleDataTypeCommunityHeader:
|
||||
collectibles, err := s.manager.FetchAssetsByCollectibleUniqueID(ctx, ids)
|
||||
collectibles, err := s.manager.FetchAssetsByCollectibleUniqueID(ctx, ids, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch dataType {
|
||||
case CollectibleDataTypeHeader:
|
||||
return s.fullCollectiblesDataToHeaders(collectibles)
|
||||
return fullCollectiblesDataToHeaders(collectibles), nil
|
||||
case CollectibleDataTypeDetails:
|
||||
return s.fullCollectiblesDataToDetails(collectibles)
|
||||
return fullCollectiblesDataToDetails(collectibles), nil
|
||||
case CollectibleDataTypeCommunityHeader:
|
||||
return s.fullCollectiblesDataToCommunityHeader(collectibles)
|
||||
return fullCollectiblesDataToCommunityHeader(collectibles), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("unknown data type")
|
||||
}
|
||||
|
||||
func idsToCollectibles(ids []thirdparty.CollectibleUniqueID) []Collectible {
|
||||
res := make([]Collectible, 0, len(ids))
|
||||
|
||||
for _, id := range ids {
|
||||
c := idToCollectible(id)
|
||||
res = append(res, c)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *Service) fullCollectiblesDataToHeaders(data []thirdparty.FullCollectibleData) ([]Collectible, error) {
|
||||
res := make([]Collectible, 0, len(data))
|
||||
|
||||
for _, c := range data {
|
||||
header := fullCollectibleDataToHeader(c)
|
||||
|
||||
ownership, err := s.ownershipDB.GetOwnership(c.CollectibleData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header.Ownership = ownership
|
||||
|
||||
if c.CollectibleData.CommunityID != "" {
|
||||
communityInfo, _, err := s.communityManager.GetCommunityInfo(c.CollectibleData.CommunityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
communityData := communityInfoToData(c.CollectibleData.CommunityID, communityInfo, c.CommunityInfo)
|
||||
header.CommunityData = &communityData
|
||||
}
|
||||
|
||||
res = append(res, header)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) fullCollectiblesDataToDetails(data []thirdparty.FullCollectibleData) ([]Collectible, error) {
|
||||
res := make([]Collectible, 0, len(data))
|
||||
|
||||
for _, c := range data {
|
||||
details := fullCollectibleDataToDetails(c)
|
||||
|
||||
ownership, err := s.ownershipDB.GetOwnership(c.CollectibleData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
details.Ownership = ownership
|
||||
|
||||
if c.CollectibleData.CommunityID != "" {
|
||||
communityInfo, _, err := s.communityManager.GetCommunityInfo(c.CollectibleData.CommunityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
communityData := communityInfoToData(c.CollectibleData.CommunityID, communityInfo, c.CommunityInfo)
|
||||
details.CommunityData = &communityData
|
||||
}
|
||||
|
||||
res = append(res, details)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) fullCollectiblesDataToCommunityHeader(data []thirdparty.FullCollectibleData) ([]Collectible, error) {
|
||||
res := make([]Collectible, 0, len(data))
|
||||
|
||||
for _, c := range data {
|
||||
collectibleID := c.CollectibleData.ID
|
||||
communityID := c.CollectibleData.CommunityID
|
||||
|
||||
if communityID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
communityInfo, _, err := s.communityManager.GetCommunityInfo(communityID)
|
||||
if err != nil {
|
||||
log.Error("Error fetching community info", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
communityData := communityInfoToData(communityID, communityInfo, c.CommunityInfo)
|
||||
|
||||
header := Collectible{
|
||||
ID: collectibleID,
|
||||
CollectibleData: &CollectibleData{
|
||||
Name: c.CollectibleData.Name,
|
||||
},
|
||||
CommunityData: &communityData,
|
||||
}
|
||||
|
||||
res = append(res, header)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) notifyCommunityCollectiblesReceived(ownedCollectibles OwnedCollectibles) {
|
||||
ctx := context.Background()
|
||||
|
||||
collectiblesData, err := s.manager.FetchAssetsByCollectibleUniqueID(ctx, ownedCollectibles.ids)
|
||||
collectiblesData, err := s.manager.FetchAssetsByCollectibleUniqueID(ctx, ownedCollectibles.ids, false)
|
||||
if err != nil {
|
||||
log.Error("Error fetching collectibles data", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
communityCollectibles, err := s.fullCollectiblesDataToCommunityHeader(collectiblesData)
|
||||
if err != nil {
|
||||
log.Error("Error converting received collectibles data", "error", err)
|
||||
return
|
||||
}
|
||||
communityCollectibles := fullCollectiblesDataToCommunityHeader(collectiblesData)
|
||||
|
||||
if len(communityCollectibles) == 0 {
|
||||
return
|
||||
|
|
|
@ -47,6 +47,17 @@ func idToCollectible(id thirdparty.CollectibleUniqueID) Collectible {
|
|||
return ret
|
||||
}
|
||||
|
||||
func idsToCollectibles(ids []thirdparty.CollectibleUniqueID) []Collectible {
|
||||
res := make([]Collectible, 0, len(ids))
|
||||
|
||||
for _, id := range ids {
|
||||
c := idToCollectible(id)
|
||||
res = append(res, c)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func fullCollectibleDataToHeader(c thirdparty.FullCollectibleData) Collectible {
|
||||
ret := Collectible{
|
||||
DataType: CollectibleDataTypeHeader,
|
||||
|
@ -66,13 +77,28 @@ func fullCollectibleDataToHeader(c thirdparty.FullCollectibleData) Collectible {
|
|||
ImageURL: c.CollectionData.ImageURL,
|
||||
}
|
||||
}
|
||||
|
||||
if c.CollectibleData.CommunityID != "" {
|
||||
communityData := communityInfoToData(c.CollectibleData.CommunityID, c.CommunityInfo, c.CollectibleCommunityInfo)
|
||||
ret.CommunityData = &communityData
|
||||
}
|
||||
ret.Ownership = c.Ownership
|
||||
return ret
|
||||
}
|
||||
|
||||
func fullCollectiblesDataToHeaders(data []thirdparty.FullCollectibleData) []Collectible {
|
||||
res := make([]Collectible, 0, len(data))
|
||||
|
||||
for _, c := range data {
|
||||
header := fullCollectibleDataToHeader(c)
|
||||
res = append(res, header)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func fullCollectibleDataToDetails(c thirdparty.FullCollectibleData) Collectible {
|
||||
ret := Collectible{
|
||||
DataType: CollectibleDataTypeHeader,
|
||||
DataType: CollectibleDataTypeDetails,
|
||||
ID: c.CollectibleData.ID,
|
||||
CollectibleData: &CollectibleData{
|
||||
Name: c.CollectibleData.Name,
|
||||
|
@ -91,9 +117,53 @@ func fullCollectibleDataToDetails(c thirdparty.FullCollectibleData) Collectible
|
|||
ImageURL: c.CollectionData.ImageURL,
|
||||
}
|
||||
}
|
||||
if c.CollectibleData.CommunityID != "" {
|
||||
communityData := communityInfoToData(c.CollectibleData.CommunityID, c.CommunityInfo, c.CollectibleCommunityInfo)
|
||||
ret.CommunityData = &communityData
|
||||
}
|
||||
ret.Ownership = c.Ownership
|
||||
return ret
|
||||
}
|
||||
|
||||
func fullCollectiblesDataToDetails(data []thirdparty.FullCollectibleData) []Collectible {
|
||||
res := make([]Collectible, 0, len(data))
|
||||
|
||||
for _, c := range data {
|
||||
details := fullCollectibleDataToDetails(c)
|
||||
res = append(res, details)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func fullCollectiblesDataToCommunityHeader(data []thirdparty.FullCollectibleData) []Collectible {
|
||||
res := make([]Collectible, 0, len(data))
|
||||
|
||||
for _, c := range data {
|
||||
collectibleID := c.CollectibleData.ID
|
||||
communityID := c.CollectibleData.CommunityID
|
||||
|
||||
if communityID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
communityData := communityInfoToData(communityID, c.CommunityInfo, c.CollectibleCommunityInfo)
|
||||
|
||||
header := Collectible{
|
||||
ID: collectibleID,
|
||||
CollectibleData: &CollectibleData{
|
||||
Name: c.CollectibleData.Name,
|
||||
},
|
||||
CommunityData: &communityData,
|
||||
Ownership: c.Ownership,
|
||||
}
|
||||
|
||||
res = append(res, header)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func communityInfoToData(communityID string, community *thirdparty.CommunityInfo, communityCollectible *thirdparty.CollectibleCommunityInfo) CommunityData {
|
||||
ret := CommunityData{
|
||||
ID: communityID,
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package collectibles
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func getCommunityCollectible() thirdparty.FullCollectibleData {
|
||||
return thirdparty.GenerateTestFullCollectiblesData(1)[0]
|
||||
}
|
||||
|
||||
func getNonCommunityCollectible() thirdparty.FullCollectibleData {
|
||||
c := thirdparty.GenerateTestFullCollectiblesData(1)[0]
|
||||
c.CollectibleData.CommunityID = ""
|
||||
c.CollectionData.CommunityID = ""
|
||||
c.CommunityInfo = nil
|
||||
c.CollectibleCommunityInfo = nil
|
||||
return c
|
||||
}
|
||||
|
||||
func TestFullCollectibleToHeader(t *testing.T) {
|
||||
communityCollectible := getCommunityCollectible()
|
||||
communityHeader := fullCollectibleDataToHeader(communityCollectible)
|
||||
|
||||
require.Equal(t, CollectibleDataTypeHeader, communityHeader.DataType)
|
||||
require.Equal(t, communityCollectible.CollectibleData.ID, communityHeader.ID)
|
||||
|
||||
require.NotEmpty(t, communityHeader.CollectibleData)
|
||||
require.NotEmpty(t, communityHeader.CollectionData)
|
||||
require.NotEmpty(t, communityHeader.CommunityData)
|
||||
require.NotEmpty(t, communityHeader.Ownership)
|
||||
|
||||
nonCommunityCollectible := getNonCommunityCollectible()
|
||||
nonCommunityHeader := fullCollectibleDataToHeader(nonCommunityCollectible)
|
||||
|
||||
require.Equal(t, CollectibleDataTypeHeader, nonCommunityHeader.DataType)
|
||||
require.Equal(t, nonCommunityCollectible.CollectibleData.ID, nonCommunityHeader.ID)
|
||||
|
||||
require.NotEmpty(t, nonCommunityHeader.CollectibleData)
|
||||
require.NotEmpty(t, nonCommunityHeader.CollectionData)
|
||||
require.Empty(t, nonCommunityHeader.CommunityData)
|
||||
require.NotEmpty(t, nonCommunityHeader.Ownership)
|
||||
}
|
||||
|
||||
func TestFullCollectibleToDetails(t *testing.T) {
|
||||
communityCollectible := getCommunityCollectible()
|
||||
communityDetails := fullCollectibleDataToDetails(communityCollectible)
|
||||
|
||||
require.Equal(t, CollectibleDataTypeDetails, communityDetails.DataType)
|
||||
require.Equal(t, communityCollectible.CollectibleData.ID, communityDetails.ID)
|
||||
|
||||
require.NotEmpty(t, communityDetails.CollectibleData)
|
||||
require.NotEmpty(t, communityDetails.CollectionData)
|
||||
require.NotEmpty(t, communityDetails.CommunityData)
|
||||
require.NotEmpty(t, communityDetails.Ownership)
|
||||
|
||||
nonCommunityCollectible := getNonCommunityCollectible()
|
||||
nonCommunityDetails := fullCollectibleDataToDetails(nonCommunityCollectible)
|
||||
|
||||
require.Equal(t, CollectibleDataTypeDetails, nonCommunityDetails.DataType)
|
||||
require.Equal(t, nonCommunityCollectible.CollectibleData.ID, nonCommunityDetails.ID)
|
||||
|
||||
require.NotEmpty(t, nonCommunityDetails.CollectibleData)
|
||||
require.NotEmpty(t, nonCommunityDetails.CollectionData)
|
||||
require.Empty(t, nonCommunityDetails.CommunityData)
|
||||
require.NotEmpty(t, nonCommunityDetails.Ownership)
|
||||
}
|
||||
|
||||
func TestFullCollectiblesToCommunityHeader(t *testing.T) {
|
||||
collectibles := make([]thirdparty.FullCollectibleData, 0, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
if i%2 == 0 {
|
||||
collectibles = append(collectibles, getCommunityCollectible())
|
||||
} else {
|
||||
collectibles = append(collectibles, getNonCommunityCollectible())
|
||||
}
|
||||
}
|
||||
|
||||
communityHeaders := fullCollectiblesDataToCommunityHeader(collectibles)
|
||||
require.Equal(t, 5, len(communityHeaders))
|
||||
}
|
|
@ -81,6 +81,10 @@ func (o *DataDB) SetCommunityInfo(id string, c *thirdparty.CommunityInfo) (err e
|
|||
}
|
||||
|
||||
func (o *DataDB) GetCommunityInfo(id string) (*thirdparty.CommunityInfo, *InfoState, error) {
|
||||
if id == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
var info thirdparty.CommunityInfo
|
||||
var state InfoState
|
||||
var row *sql.Row
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package community
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
|
@ -19,27 +18,11 @@ func setupCommunityDataDBTest(t *testing.T) (*DataDB, func()) {
|
|||
}
|
||||
}
|
||||
|
||||
func generateTestCommunityInfo(count int) map[string]thirdparty.CommunityInfo {
|
||||
result := make(map[string]thirdparty.CommunityInfo)
|
||||
for i := 0; i < count; i++ {
|
||||
communityID := fmt.Sprintf("communityid-%d", i)
|
||||
newCommunity := thirdparty.CommunityInfo{
|
||||
CommunityName: fmt.Sprintf("communityname-%d", i),
|
||||
CommunityColor: fmt.Sprintf("communitycolor-%d", i),
|
||||
CommunityImage: fmt.Sprintf("communityimage-%d", i),
|
||||
CommunityImagePayload: []byte(fmt.Sprintf("communityimagepayload-%d", i)),
|
||||
}
|
||||
result[communityID] = newCommunity
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func TestUpdateCommunityInfo(t *testing.T) {
|
||||
db, cleanup := setupCommunityDataDBTest(t)
|
||||
defer cleanup()
|
||||
|
||||
communityData := generateTestCommunityInfo(10)
|
||||
communityData := thirdparty.GenerateTestCommunityInfo(10)
|
||||
extraCommunityID := "extra-community-id"
|
||||
|
||||
for communityID, communityInfo := range communityData {
|
||||
|
|
|
@ -2,16 +2,12 @@ package community
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/status-im/status-go/server"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
const failedCommunityFetchRetryDelay = 1 * time.Hour
|
||||
|
||||
type Manager struct {
|
||||
db *DataDB
|
||||
communityInfoProvider thirdparty.CommunityInfoProvider
|
||||
|
@ -53,36 +49,7 @@ func (cm *Manager) setCommunityInfo(id string, c *thirdparty.CommunityInfo) (err
|
|||
return cm.db.SetCommunityInfo(id, c)
|
||||
}
|
||||
|
||||
func (cm *Manager) mustFetchCommunityInfo(communityID string) bool {
|
||||
// See if we have cached data
|
||||
_, state, err := cm.GetCommunityInfo(communityID)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// If we don't have a state, this community has never been fetched before
|
||||
if state == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the last fetch was successful, we can safely refresh our cache
|
||||
if state.LastUpdateSuccesful {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the last fetch was not successful, we should only retry after a delay
|
||||
if time.Unix(int64(state.LastUpdateTimestamp), 0).Add(failedCommunityFetchRetryDelay).Before(time.Now()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (cm *Manager) FetchCommunityInfo(communityID string) (*thirdparty.CommunityInfo, error) {
|
||||
if !cm.mustFetchCommunityInfo(communityID) {
|
||||
return nil, fmt.Errorf("backing off fetchCommunityInfo for id: %s", communityID)
|
||||
}
|
||||
|
||||
communityInfo, err := cm.communityInfoProvider.FetchCommunityInfo(communityID)
|
||||
if err != nil {
|
||||
dbErr := cm.setCommunityInfo(communityID, nil)
|
||||
|
|
|
@ -194,6 +194,7 @@ func (c *Asset) toCollectiblesData(id thirdparty.CollectibleUniqueID) thirdparty
|
|||
ImageURL: c.Image.ImageURL,
|
||||
AnimationURL: c.Image.CachedAnimationURL,
|
||||
Traits: alchemyToCollectibleTraits(rawMetadata.Attributes),
|
||||
TokenURI: c.TokenURI,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -146,9 +146,11 @@ type CollectibleCommunityInfo struct {
|
|||
// Combined Collection+Collectible info returned by the CollectibleProvider
|
||||
// Some providers may not return the CollectionData in the same API call, so it's optional
|
||||
type FullCollectibleData struct {
|
||||
CollectibleData CollectibleData
|
||||
CollectionData *CollectionData
|
||||
CommunityInfo *CollectibleCommunityInfo
|
||||
CollectibleData CollectibleData
|
||||
CollectionData *CollectionData
|
||||
CommunityInfo *CommunityInfo
|
||||
CollectibleCommunityInfo *CollectibleCommunityInfo
|
||||
Ownership []AccountBalance
|
||||
}
|
||||
|
||||
type CollectiblesContainer[T any] struct {
|
||||
|
|
|
@ -9,9 +9,9 @@ type CommunityInfo struct {
|
|||
}
|
||||
|
||||
type CommunityInfoProvider interface {
|
||||
GetCommunityID(tokenURI string) string
|
||||
FetchCommunityInfo(communityID string) (*CommunityInfo, error)
|
||||
|
||||
// Collectible-related methods
|
||||
GetCommunityID(tokenURI string) string
|
||||
FillCollectibleMetadata(collectible *FullCollectibleData) error
|
||||
}
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
package thirdparty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/protocol/communities/token"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
w_common "github.com/status-im/status-go/services/wallet/common"
|
||||
)
|
||||
|
||||
func GenerateTestCollectiblesData(count int) (result []CollectibleData) {
|
||||
base := rand.Intn(100) // nolint: gosec
|
||||
|
||||
result = make([]CollectibleData, 0, count)
|
||||
for i := base; i < count+base; i++ {
|
||||
bigI := big.NewInt(int64(i))
|
||||
newCollectible := CollectibleData{
|
||||
ID: CollectibleUniqueID{
|
||||
ContractID: ContractID{
|
||||
ChainID: w_common.ChainID(i % 4),
|
||||
Address: common.BigToAddress(bigI),
|
||||
},
|
||||
TokenID: &bigint.BigInt{Int: bigI},
|
||||
},
|
||||
Provider: fmt.Sprintf("provider-%d", i),
|
||||
Name: fmt.Sprintf("name-%d", i),
|
||||
Description: fmt.Sprintf("description-%d", i),
|
||||
Permalink: fmt.Sprintf("permalink-%d", i),
|
||||
ImageURL: fmt.Sprintf("imageurl-%d", i),
|
||||
ImagePayload: []byte(fmt.Sprintf("imagepayload-%d", i)),
|
||||
AnimationURL: fmt.Sprintf("animationurl-%d", i),
|
||||
AnimationMediaType: fmt.Sprintf("animationmediatype-%d", i),
|
||||
Traits: []CollectibleTrait{
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
},
|
||||
BackgroundColor: fmt.Sprintf("backgroundcolor-%d", i),
|
||||
TokenURI: fmt.Sprintf("tokenuri-%d", i),
|
||||
CommunityID: fmt.Sprintf("communityid-%d", i%5),
|
||||
}
|
||||
result = append(result, newCollectible)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GenerateTestCollectiblesCommunityData(count int) []CollectibleCommunityInfo {
|
||||
base := rand.Intn(100) // nolint: gosec
|
||||
|
||||
result := make([]CollectibleCommunityInfo, 0, count)
|
||||
for i := base; i < count+base; i++ {
|
||||
newCommunityInfo := CollectibleCommunityInfo{
|
||||
PrivilegesLevel: token.PrivilegesLevel(i) % (token.CommunityLevel + 1),
|
||||
}
|
||||
result = append(result, newCommunityInfo)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GenerateTestCollectiblesOwnership(count int) []AccountBalance {
|
||||
base := rand.Intn(100) // nolint: gosec
|
||||
|
||||
ret := make([]AccountBalance, 0, count)
|
||||
for i := base; i < count+base; i++ {
|
||||
ret = append(ret, AccountBalance{
|
||||
Address: common.HexToAddress(fmt.Sprintf("0x%x", i)),
|
||||
Balance: &bigint.BigInt{Int: big.NewInt(int64(i))},
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func GenerateTestCollectionsData(count int) (result []CollectionData) {
|
||||
base := rand.Intn(100) // nolint: gosec
|
||||
|
||||
result = make([]CollectionData, 0, count)
|
||||
for i := base; i < count+base; i++ {
|
||||
bigI := big.NewInt(int64(count))
|
||||
traits := make(map[string]CollectionTrait)
|
||||
for j := 0; j < 3; j++ {
|
||||
traits[fmt.Sprintf("traittype-%d", j)] = CollectionTrait{
|
||||
Min: float64(i+j) / 2,
|
||||
Max: float64(i+j) * 2,
|
||||
}
|
||||
}
|
||||
|
||||
newCollection := CollectionData{
|
||||
ID: ContractID{
|
||||
ChainID: w_common.ChainID(i),
|
||||
Address: common.BigToAddress(bigI),
|
||||
},
|
||||
Provider: fmt.Sprintf("provider-%d", i),
|
||||
Name: fmt.Sprintf("name-%d", i),
|
||||
Slug: fmt.Sprintf("slug-%d", i),
|
||||
ImageURL: fmt.Sprintf("imageurl-%d", i),
|
||||
ImagePayload: []byte(fmt.Sprintf("imagepayload-%d", i)),
|
||||
Traits: traits,
|
||||
CommunityID: fmt.Sprintf("community-%d", i),
|
||||
}
|
||||
result = append(result, newCollection)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GenerateTestCommunityInfo(count int) map[string]CommunityInfo {
|
||||
base := rand.Intn(100) // nolint: gosec
|
||||
|
||||
result := make(map[string]CommunityInfo)
|
||||
for i := base; i < count+base; i++ {
|
||||
communityID := fmt.Sprintf("communityid-%d", i)
|
||||
newCommunity := CommunityInfo{
|
||||
CommunityName: fmt.Sprintf("communityname-%d", i),
|
||||
CommunityColor: fmt.Sprintf("communitycolor-%d", i),
|
||||
CommunityImage: fmt.Sprintf("communityimage-%d", i),
|
||||
CommunityImagePayload: []byte(fmt.Sprintf("communityimagepayload-%d", i)),
|
||||
}
|
||||
result[communityID] = newCommunity
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func GenerateTestFullCollectiblesData(count int) []FullCollectibleData {
|
||||
collectiblesData := GenerateTestCollectiblesData(count)
|
||||
collectionsData := GenerateTestCollectionsData(count)
|
||||
communityInfoMap := GenerateTestCommunityInfo(count)
|
||||
communityInfo := make([]CommunityInfo, 0, count)
|
||||
for _, info := range communityInfoMap {
|
||||
communityInfo = append(communityInfo, info)
|
||||
}
|
||||
communityData := GenerateTestCollectiblesCommunityData(count)
|
||||
|
||||
ret := make([]FullCollectibleData, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
ret = append(ret, FullCollectibleData{
|
||||
CollectibleData: collectiblesData[i],
|
||||
CollectionData: &collectionsData[i],
|
||||
CommunityInfo: &communityInfo[i],
|
||||
CollectibleCommunityInfo: &communityData[i],
|
||||
Ownership: GenerateTestCollectiblesOwnership(rand.Intn(5) + 1), // nolint: gosec
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
Loading…
Reference in New Issue